Sort Algorithm(ASC)
[TOC] //怎么生成目录,纠结ing
插入排序
每一趟排序都将待排元素插入到已有序的序列中,但不能保证该元素的插入位置是全局有序的。
Insertion Sort
直接插入排序
- 最好情况:$O(n)$
- 最坏情况:$O(n^2)$
- 平均情况:$O(n^2)$
- 辅助空间:O(1)
- 稳定性:稳定
- 每一趟排序只能保证当前有序,并不能保证全局有序
核心思想
数组中的每一个待排元素与已部分有序的元素依次比较,如果待排元素小于当前已有序的元素,则交换两者位置,并继续向前比较。-
程序示例
public static void insertionSort(int[] arr){ for (int i=1;i<arr.length;i++){ for (int j=i-1;j>=0;j--){ if (arr[j]>arr[j+1]){ swap(arr,j,j+1); } } } } public static void swap(int[] arr,int i,int j){ int tmp=arr[i]; arr[i]=arr[j]; arr[j]=tmp; }
-
程序分析
直接插入排序采用两层循环:- 外层循环用来跟踪每一个待排元素,从1到 n-1,刚开始arr[0]是默认有序的。
- 内层循环负责当前待排元素与已部分有序的元素进行逐一比较,确定当前待排元素的最终插入位置。
优化分析*
在内层循环中,每当逆序(已有序元素大于待排元素时)的情况出现时,就执行一次 swap 操作,交换两元素的位置,这样会导致交换次数较多。可以考虑通过比较确定最后的插入位置,然后将交换操作推迟到最后进行。这样只需要进行一次交换操作,但已有序元素的移动次数是不会改变的。
Shell Sort
希尔排序(缩小增量排序)
- 步长+直接插入排序
- 最好情况:$O(n)$
- 最坏情况:$O(n^2)$
- 平均情况:$O(n^{1.3})$
- 辅助空间:O(1)
- 稳定性:不稳定
- 每一趟排序只能保证当前有序,并不能保证全局有序
核心思想
希尔排序,又称缩小增量排序,使用步长来确定每一次的待排子序列,然后使用直接插入排序对当前待排子序列进行排序。在完成步长为 h 的排序后,通过证明可以保证,对于每一个 i 与 i+h 元素都是有序的。然后缩小步长,再次使用直接插入排序。直到最后步长为1,最后一次序列已经基本有序,最后采用一次直接插入排序后就可以得到最终的有序数组。-
程序示例
public static void shellSort(int[] arr){ //第一层循环控制步长 for (int gap=arr.length/2;gap>0;gap=gap/2){ //第二层循环控制满足步长的所有子序列 for (int i=gap;i<arr.length;i++){ //第三层循环控制当前满足步长的子序列的排序,采用直接插入排序的方式进行排序 for (int j=i;j<arr.length;j+=gap){ if (arr[j]<arr[j-gap]){ swap(arr,j,j-gap); } } } } } public static void swap(int[] arr,int i,int j){ int tmp=arr[i]; arr[i]=arr[j]; arr[j]=tmp; }
-
程序分析
希尔排序采用三层循环:- 第一层循环负责确定每一趟的步长,并按一定的规则(这里采用/2折半的规则),循环减小步长到1。
- 第二层循环针对当前步长,依次确定满足该步长的待排子序列。
- 第三层循环针对当前确定的待排子序列,采用直接插入排序的方法进行排序。
选择排序
基本思想:比较+交换
Selection Sort
简单选择排序
- 最好情况:$O(n^2)$
- 最坏情况:$O(n^2)$
- 平均情况:$O(n^2)$
- 辅助空间:O(1)
- 稳定性:不稳定
- 每一趟排序保证全局有序
核心思想
简单选择排序,是选择排序中的一种,遵循其基本思想:比较+交换。即通过依次比较确定当前趟最小元素,并根据情况与当前待排元素交换。具体做法是在每趟排序中,通过将当前待排元素与与剩余所有元素比较,找到最小的元素并与当前元素交换(如果当前待排元素就是最小,则不用交换)。-
程序示例
public static void selectionSort(int[] arr){ for (int i=0;i<arr.length;i++){ //这里采用 min 来标记最小元素的下标,方便 swap 的时候通过下标进行交换 int min=i; //第二层循环负责将当前元素与剩余所有元素比较,找到这些元素中的最小值,如果该最小值不是当前元素,则在退出循环后将当前元素与该最小元素交换 //因为每一次寻找当前趟最小元素都是与所有待排元素比较,因此,简单选择排序是全局有序的 for (int j=i+1;j<arr.length;j++){ if (arr[j]<arr[min]){ min=j; } } if (min!=i){ swap(arr,i,min); } } } public static void swap(int[] arr,int i,int j){ int tmp=arr[i]; arr[i]=arr[j]; arr[j]=tmp; }
-
程序分析
简单选择排序,采用两层循环:- 外层循环用于控制当前待排元素及其待排位置。
- 内存循环用于基于当前待排位置和待排元素,循环依次与序列中剩余其它元素比较,如果存在元素小于当前待排元素,则采用一个临时变量始终标记符合该条件的元素。在退出内层循环后,判断经过一趟完整比较后,最小元素是否为当前待排元素,如果是,说明当前待排元素是当前趟的最小值,当前位置即全局有序位置,不交换;如果不是,则采用 swap 交换。
Heap Sort
堆排序
- 最好情况:$O(nlog_2n)$
- 最坏情况:$O(nlog_2n)$
- 平均情况:$O(nlog_2n)$
- 辅助空间:O(1)
- 稳定性:不稳定
- 每一趟排序保证全局有序
核心思想
堆排序,先根据父节点与子节点下标关系(i,2i+1,2i+2)将待排序列构造为大顶堆(实际上还是在数组中存储)。然后执行循环操作:每次将顶点元素与最后一个元素swap,交换后,当前最大元素已经位于数组的末尾,因为原来的末尾元素到了顶点位置,有可能破坏堆的性质,则通过堆调整来修正堆性质。循环执行这一组操作,直到堆中只有一个元素的时候,该待排序列已然有序。-
程序示例
public static void heapSort(int[] arr){ buildHeap(arr); int length=arr.length; while (length>0){ swap(arr,0,length-1); length--; adjustToHeap(arr,length,0); } } public static void buildHeap(int[] arr){ for (int i=0;i<arr.length/2;i++){ adjustToHeap(arr,arr.length,i); } } public static void adjustToHeap(int[] arr,int length,int index){ int indexOfMax=index; while (true){ int leftIndex=getLeftChild(index); int rightIndex=getRightChild(index); if (leftIndex<length&&arr[leftIndex]>arr[indexOfMax]){ indexOfMax=leftIndex; } if (rightIndex<length&&arr[rightIndex]>arr[indexOfMax]){ indexOfMax=rightIndex; } if (indexOfMax!=index){ swap(arr,index,indexOfMax); index=indexOfMax; continue; } else { break; } } } public static int getLeftChild(int i){ return 2*i+1; } public static int getRightChild(int i){ return 2*i+2; } public static void swap(int[] arr,int i,int j){ int tmp=arr[i]; arr[i]=arr[j]; arr[j]=tmp; }
-
程序分析
在采用堆排序前,需要对待排序列构造大顶堆。具体构造方式,稍后详述。- 在生成大顶堆后,满足大顶堆的性质,第一个元素 arr[0]是当前0~arr.length-1中的最大值。接下来对该大顶堆进行 N-1 次的交换和调整。
- 具体做法是,在每次交换和调整中,将堆顶的最大值与堆尾元素互换,这样,该最大值就位于正确顺序位置上。因为互换后有可能破坏大顶堆顺序,因此必须在互换后调用 adjustToHeap 进行堆性质修正。
交换排序
Bubble Sort
冒泡排序
- 最好情况:$O(n^2)$
- 最坏情况:$O(n^2)$
- 平均情况:$O(n^2)$
- 辅助空间:O(1)
- 稳定性:稳定
- 每一趟排序保证全局有序
核心思想
冒泡排序,在每一趟排序中,都依次比较左右两个元素,如果不符合大小关系,则交换位置,并继续比较下一个坐标位置。在一趟比较结束后,当前趟最大的元素会沉底。-
程序示例
public static void bubbleSort(int[] arr){ //对于 N 个元素的序列,需要 N-1 趟排序 for (int i=1;i<arr.length;i++){ //在每一趟排序后,下标为 arr.length-i的元素已经位于全局有序位置,因此下一趟比较的最后待排位置为 arr.length-i-1 //每一趟比较中,当前下标的左右两个元素如果顺序不当,则互相交换,较大的向后调整,如果满足大小要求,则不交换。这样依次对剩余元素进行比较,直到 arr.length-i for (int j=0;j<arr.length-i;j++){ if (arr[j]>arr[j+1]){ swap(arr,j,j+1); } } } } public static void swap(int[] arr,int i,int j){ int tmp=arr[i]; arr[i]=arr[j]; arr[j]=tmp; }
-
程序分析
冒泡排序采用两层循环,对于 N 个元素的冒泡排序,需要进行 N-1 趟排序,第 i 趟排序需要比较 N-i 次。- 第一层循环控制冒泡排序的趟数,从1到 N-1趟冒泡排序。
- 第二层循环控制每一趟的冒泡排序,因为经过第 i-1 趟的排序,第 N-i+1 个位置已经有序,所以在第 i 趟的冒泡排序中,只需要从第一个元素一直比较到第 N-i 个元素。
优化分析
针对最好情况为$O(n)$的情形,可以在外层循环中设置一个变量用来检查上一趟是否存在交换操作,在内层循环中,发生 swap 操作则置该变量为 true,否则默认为 false。在退出内层循环后,外层逻辑会判断如果变量为 true 说明上一趟待排序列不满足顺序条件,继续本趟排序;如果为 false 说明在上一趟中待排序列所有元素已经有序。因此在本趟排序可以直接 break,已经提前完成了序列排序。
Quick Sort
快速排序
- 最好情况:$O(nlog_2n)$
- 最坏情况:$O(n^2)$
- 平均情况:$O(nlog_2n)$
- 辅助空间:$O(nlog_2n)$
- 稳定性:不稳定
- 每一趟排序保证轴 key 全局有序
核心思想
循环比较交换+分而治之的思想。在每一趟的排序操作中,当 i不等于j 的时候,将大于 key 的元素都交换在 key 的右边,小于 key 的元素都交换在 key 的左边。当 i等于j 的时候,就是 key 的最终有序位置。但是经过一趟排序操作,仅保证了当前 key 位于正确有序的位置 i,但对于start~i-1和 i+1~end 是无序的,因此需要在这两段上递归调用刚刚的排序操作。-
程序示例
public static void quickSort(int[] arr,int start,int end){ if (start<end){ int i=start; int j=end; int key=arr[start]; while (i!=j){ //从后向前循环找到第一个小于 key 的元素 while (i<j&&arr[j]>=key){ j--; } //将该元素替换到 key 的左边 if (i<j){ arr[i]=arr[j]; i++; } //从前向后循环找第一个大于 key 的元素 while (i<j&&arr[i]<key){ i++; } //将该元素替换到 key 的右边 if (i<j){ arr[j]=arr[i]; j--; } } //当 i=j 的时候,说明在这一趟交换过后,所有大于 key 的元素都位于 key 的右边,所有小于 key 的元素都位于 key 的左边 //而此时 i==j 即表示 key 应该插入的位置,即原先的 arr[start]元素的最终位置 arr[i]=key; quickSort(arr,start,i-1); quickSort(arr,i+1,end); } }
-
程序分析
快速排序在每一趟排序中,先选定一个轴,通过交换来确定轴的最终位置,在一趟排序后,大于或小于该轴的元素会位于该轴的两侧。- 外层循环用来控制这趟排序是否结束,当 i 等于 j 的时候,说明待排序列的比较已经结束,该位置即为轴 key 的最终有序位置。
- 在第一个内层循环中,先从后向前循环查找第一个小于 key 的元素,如果存在则退出循环并将该元素交换至 key 的 i 位置,然后再进入第二个内层循环。如果没有存在一个小于 key 的元素,说明在从后向前的查找中,key 后面的元素都大于 key,因此,说明 key 就是在正确的有序位置。
- 在第二个内层循环中,从前向后循环查找第一个大于 key 的元素,如果存在则退出循环并交换至当前 j 位置,然后退出该循环,判断 i 是否等于 j,不等于则说明该趟还没比较结束,继续从第一个内层循环开始,继续排序操作。直至 i 等于 j。
优化分析
快速排序对于越无序的序列效果越好,但对于基本有序序列,在选定 key 后,有可能剩余的 N-1个元素均大于或小于 key,导致分而治之的效果不明显,没有明显的均分待排序列,使得效率接近于插入排序$O(n^2)$的效果。改善这样的情况,可以尝试采用随机 key 的方式,每一趟排序,均在当前 start~end 中随机选取 key 作为轴,而不是始终设定为 arr[start]为轴,尽可能通过随机化减少有序影响,实现或接近分治效果。