排序算法学习
前言
算法,其实算术题的解法,如同我们读书时做的数学题。一道关于排序的算术题,有几种解法就有几种思路。一般程序员可能一辈子都用不上排序算法,但是我们可以学习其中的解题思路,融会贯通后,可以对我们实际开发有指导作用。
题目
数量为10的无序数组,将其按照升序排列。例如
选择排序
如果这样的数组放在面前,要你手工排序,一般人都是先浏览一遍,找出最小的19拿走,再浏览剩下的,再拿走最小的22,直到全部取走,到手的就是有序集合。其实这种做法就是选择排序法。
当然,程序实现上为了节省内存,会用交换代替取的。如
public class SelectSort { public static int[] sort(int[] sourceArray) { int[] array = Arrays.copyOf(sourceArray, sourceArray.length); int length = array.length; for (int i = 0; i < length-1; i++) { int min = i; for (int j = i + 1; j < length; j++) { if (array[min] > array[j]) { min = j; } } swap(array,i,min); } return array; } static void swap(int[] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } }
冒泡排序
冒泡排序形象点描述是用泡泡装着最右的元素,不断向左前进比较,泡泡里面的元素较大就交换,较小就继续前进,直到到达最左。
public class BubbleSort { public static int[] sort(int[] sourceArray) { int[] array = Arrays.copyOf(sourceArray, sourceArray.length); int length = array.length; for (int i = 0; i < length; i++) { for (int j = length - 1; j > i; j--) { if (array[j] < array[j - 1]) { swap(array, j, j - 1); } } } return array; } static void swap(int[] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } }
插入排序
插入排序的主要思路是将数组分成有序和无序两虚拟集合。
初始状态时,38是有序集合,19到66都是无序集合。
然后将无序集合的第一个元素19存起来,从右往左对比有序集合中的元素,如
和有序集合中的38比较,38较大,移动38到下一个位置,当19走到有序集合的尽头或者遇到比19小的元素时,插入19,如
现在19和38是有序集合的元素,其余的都是无序集合,循环上述操作,直到无序集合的最后一个元素。代码实现如下
public class InsertSort { public static int[] sort(int[] sourceArray) { int[] array = Arrays.copyOf(sourceArray, sourceArray.length); int length = array.length; for (int i = 1; i < length; i++) { int tmp = array[i]; int index = i; for (int j = i - 1; j >= 0; j--) { if (tmp < array[j]) { array[j + 1] = array[j]; index = j; } else { break; } } array[index] = tmp; } return array; } }
希尔排序
上面的插入排序是通过不断移动较大的元素后移以达到排序的目的,例如4要移动最前面,要移动5次前面的元素。如果要优化插入排序的话,可以从减少移动次数下手,所以希尔排序应运而生。
希尔排序引入步长的概念,将数组分成数个虚拟数组,例如步长为10/2=5,那么分组后如下,同色的为一组
然后每一组使用插入排序法排序后,如
然后再缩小步长,例如步长为5/2向下取整2,分成
再对这两组进行插入排序,如
缩小步长,步长为2/1=1,这个时候就相当于普通的插入排序。
代码实现上,和插入排序相似,只不过再最外层加多一个缩小步长的循环,直到步长小于1,如
public class ShellSort { public static int[] sort(int[] sourceArray) { int[] array = Arrays.copyOf(sourceArray, sourceArray.length); int gap = array.length/2; while (gap >= 1) { for (int i = gap; i< array.length; i++) { int tmp = array[i]; int index = i; for (int j = i-gap; j >= 0; j = j - gap) { if (tmp < array[j]) { array[j + gap] = array[j]; index = j; } else { break; } } array[index] = tmp; } gap = gap / 2; } return array; } }
快速排序
顾名思义,在一般情况下,快速排序是速度最快的排序。
它的解题思路是选一个基准值出来,例如数组第一个位置38,通过移动位置分成三个虚拟数组,左边是小于等于38的,中间是38,右边是大于等于38的。
首先先从右边遍历和对比大小,直到找到小于38的值停下,然后从左边遍历,直到找到大于38的停下,如
这个时候交换左右位置,如下
右边继续遍历,找到小于38的21值停下来,左边开始遍历,到21的位置时,发现左边等于右边,也停下来
这个时候,除了左右到达的位置,左边走过的路都是小于等于38的,右边走过的路都是都是大于等于38,那么只要将基准值和21交换就完成三个虚拟数组的划分了
之后,将左右数组重复上面步骤,最终会得到一个有序数组。
public class QuickSort { public static int[] sort(int[] sourceArray) { int[] array = Arrays.copyOf(sourceArray, sourceArray.length); quickSort(array, 0, sourceArray.length - 1); return array; } static void quickSort(int[] array, int left, int right) { if (left >= right) { return; } int low = left; int high = right; int tmp = array[left]; while (left < right) { while (left < right && array[right] >= tmp) { right--; } while (left < right && array[left] <= tmp) { left++; } if (left != right) { swap(array, left, right); } } swap(array, low, left); quickSort(array, low, left - 1); quickSort(array, left + 1, high); } static void swap(int[] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } }
归并排序
归并排序常见于数据库排序,将扫描的数据生成多个磁盘文件,再归并排序。
它的主要思路比较简单粗暴,就是是将数组通过递归地方式拆分到单个元素,再通过不断两两合并成为有序数组。
我觉得它的精髓在于递归方式的运用,发散性地递归。
public class MergeSort { public static int[] sort(int[] sourceArray) { int[] array = Arrays.copyOf(sourceArray, sourceArray.length); int length = array.length; if (length == 1) { return array; } int middle = (int) Math.floor(length / 2); int[] leftArray = Arrays.copyOfRange(sourceArray, 0, middle); int[] rightArray = Arrays.copyOfRange(sourceArray, middle, length); return merge(sort(leftArray), sort(rightArray)); } static int[] merge(int[] leftArray, int[] rightArray) { int[] array = new int[leftArray.length + rightArray.length]; int i = 0, j = 0, k = 0; while (i < leftArray.length && j < rightArray.length) { if (leftArray[i] < rightArray[j]) { array[k++] = leftArray[i++]; } else { array[k++] = rightArray[j++]; } } while (i < leftArray.length) { array[k++] = leftArray[i++]; } while (j < rightArray.length) { array[k++] = rightArray[j++]; } return array; } }
堆排序
每一个数组都可以看成一颗完全二叉树,特点是每一个非叶子节点i,左子节点在数组的2i+1位置上,右节点在2i+2位置。简单来说,将数组从上往下从左往右铺满,可以看到非叶子节点都是在数组的前面,如下
而堆也是完全二叉树的一种,分为最大堆和最小堆,最大堆的特点是每一个父节点都大于等于子节点,所以在根节点的元素在数组的第一个位置,同时也是最大值。
所以堆排序的解题思路就是,从最后一个非叶子节点遍历,比较子节点大小,交换最大值位置,就这样将数组调整成最大堆,拿走根节点放在数组的末尾,第二次将剩余的元素继续调整成堆,拿走根节点放在数组的次末尾,重复上述操作,直到剩余元素只有一个。
public class HeapSort { public static int[] sort(int[] sourceArray) { int[] array = Arrays.copyOf(sourceArray, sourceArray.length); for (int i = array.length - 1; i > 0; i--) { buildMaxHeap(array, i); swap(array, 0, i); } return array; } static void buildMaxHeap(int[] array, int lastLeaf) { //计算最后一个非叶子节点的位置 int lastNonLeaf; if (lastLeaf % 2 == 1) { lastNonLeaf = (lastLeaf - 1) / 2; } else { lastNonLeaf = lastLeaf / 2 - 1; } for (int i = lastNonLeaf; i >= 0; i--) { int left = 2 * i + 1; int right = 2 * i + 2; if (array[left] > array[i]) { swap(array, left, i); } if (right <= lastLeaf && array[right] > array[i]) { swap(array, right, i); } } } static void swap(int[] array, int i, int j) { int temp = array[i]; array[i] = array[j]; array[j] = temp; } }
测试代码 https://github.com/mycaizilin/sort