排序算法实现以及性能分析
一、排序的分类
1、插入排序法
直接(简单)插入排序、希尔排序
2、交换排序法
冒泡排序、快速排序
3、选择排序法
直接(简单)选择排序、堆排序
4、归并排序
二路归并、多路归并
5、其他线性排序
基数排序、桶排序、计数排序
二、算法复杂度
稳定性是指如果存在多个具有相同排序码的记录,经过排序后,这些记录的相对次序仍然保持不变,则这种排序算法称为稳定的(简单的说,要排序的数中有两个相同的数,用A算法进行排序后,两个相等数的位置不会互换,则A算法是稳定的)
不稳定的:快速排序、希尔排序、选择排序、堆排序(快些选一堆)
排序方法 | 备注 |
直接插入排序 | 大部分已排序时较好(简单) |
希尔排序 | n小时较好(较复杂) |
直接选择排序 | n小时较好(简单) |
堆排序 | n大时较好(较复杂) |
冒泡排序 | n小时较好(简单) |
快速排序 | n大时较好,基本有序时反而不好(较复杂) |
归并排序 | n大时较好(较复杂) |
三、应用场景
(1)若n较小(如n<=50),可直接采用直接插入排序或直接选择排序
当记录规模小,直接插入排序较好;否则因为直接选择移动的记录数少于直接插入,应选直接选择排序为宜
(2)若文件初始状态基本有序(指正序),则应选用直接插入、冒泡排序为宜
(3)若n较大,则应采用时间复杂度为O(nlog2n)的排序方法:快速排序、堆排序或归并排序(2为底数下标)
快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
堆排序所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏的情况,这两种排序都是不稳定的;
若要求稳定的排序,则可以选用归并排序。但前面介绍的从单个记录起进行两两归并的排序算法并不值得提倡,通常可以将它和直接插入排序结合在一起使用。先利用直接插入排序求得较长的有序子序列,然后再两两归并之。因为直接插入排序是稳定 的,所以改进后的归并排序仍是稳定的。
四、性能分析
1、O(n^2)性能分析
平均性能为O(n^2)的有:直接插入排序,选择排序,冒泡排序
在数据规模较小时,直接插入排序,选择排序差不多。当数据较大时,冒泡排序算法的代价最高。
2、O(nlog2n)(2为底数下标)性能分析
平均性能为O(nlog2n)的有:快速排序,归并排序,希尔排序,堆排序。
其中,快速排序的性能是最好的,其次是归并排序和希尔排序,堆排序在数据量很时效果明显(堆排序适合处理大数据)
五、排序算法描述与实现
1、交换排序
(1)冒泡排序
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
算法描述:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 重复步骤1~3,直到排序完成。 注意:比较完一遍之后下次比较会少一次,因为最后一个数一定是最大的,所以不必要管最后一个数了
代码实现:
private static <T extends Comparable<? super T>> void maoPao(T [] a){
for(int i=0;i<a.length-1;i++){//作用设置是下一次遍历比较次数 for(int j=0;j<a.length-1-i;j++){ if(a[j].compareTo(a[j+1])>0){ T tmp = a[j]; a[j]=a[j+1]; a[j+1]=tmp; } } } }
改进的冒泡排序:
设置一个标志位flag,如何一次比较没有交换位置则flag=false,则退出while循环,完成排序
private static <T extends Comparable<? super T>> void maoPaoQuiet(T [] a){ boolean flag = true; int n=a.length; while(flag){ flag = false; for(int j=0;j<n-1;j++){ if(a[j].compareTo(a[j+1])<=0){ T tmp = a[j]; a[j]=a[j+1]; a[j+1]=tmp; flag=true; } } n--;//遍历完下次少比较一次 } }
(2)快速排序
快速排序通过一趟排序将待排序记录分割成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则分别对这两部分继续进行排序,直到整个序列有序
算法描述:
把整个序列看做一个数组,把第零个位置看做中轴,和最后一个比,如果比它小交换,比它大不做任何处理;交换了以后再和小的那端比,比它小不交换,比他大交换。这样循环往复,一趟排序完成,左边就是比中轴小的,右边就是比中轴大的,然后再用分治法,分别对这两个独立的数组进行排序。
代码实现:
private static void sort(int [] a,int low,int high){ int start =low; int end = high; int key = a[low]; while(end>start){ while(end>start&&a[end]>=key) //如果没有比关键值小的,比较下一个,直到有比关键值小的交换位置,然后又从前往后比较 end--; if(a[end]<=key){ int temp = a[end]; a[end] = a[start]; a[start] = temp; } System.out.println("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); for(int i =0;i<a.length;i++){ System.out.println(a[i]); } System.out.println("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"); //从前往后比较 while(end>start&&a[start]<=key)//如果没有比关键值大的,比较下一个,直到有比关键值大的交换位置 start++; if(a[start]>=key){ int temp = a[start]; a[start] = a[end]; a[end] = temp; } System.out.println("cccccccccccccccccccccccc"); for(int i =0;i<a.length;i++){ System.out.println(a[i]); } System.out.println("dddddddddddddddddddddddddddddd"); //此时第一次循环比较结束,关键值的位置已经确定了。左边的值都比关键值小,右边的值都比关键值大,但是两边的顺序还有可能是不一样的,进行下面的递归调用 } if(start>low) sort(a,low,start-1);//左边序列。第一个索引位置到关键值索引-1 if(end<high) sort(a,end+1,high);//右边序列。从关键值索引+1到最后一个 }
2、插入排序
(1)直接插入排序
插入排序由N-1趟排序组成。对于P=1到N-1趟,插入排序保证从位置0到位置P上的元素为已排序状态。
算法描述:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
代码实现:
public static <T extends Comparable<? super T>> void insertionSort(T[] a) { int j; for (int p = 1; p < a.length; p++) { T tmp = a[p]; for (j = p; j > 0 && tmp.compareTo(a[j - 1]) < 0; j--) { a[j] = a[j - 1]; } a[j] = tmp; } }
(2)希尔排序
希尔排序使用一个序列h1,h2,...,ht,叫做增量序列。只要h1=1,任何增量序列都是可行的,在使用增量hk的一趟排序之后,对于每一个i我们都有a[i]<=a[i+hk];所有相隔hk的元素都被排序
算法描述:
- 先求出增量序列hk
- 然后根据增量序列得到hk排序,即所有相隔hk的元素都被排序
- 之后再按照所有增量序列都得到排序结果,直到hk=1,排序完之后则为最后排序结果
代码实现:
public static <AnyType extends Comparable<? super AnyType>> void shellsort(AnyType[] a) { int j; for (int gap = a.length / 2; gap > 0; gap /= 2) for (int i = gap; i < a.length; i++) { AnyType tmp = a[i]; for (j = i; j >= gap && tmp.compareTo(a[j - gap]) < 0; j -= gap) a[j] = a[j - gap]; a[j] = tmp; } }
3、选择排序
(1)堆排序
堆排序是一种树形选择排序 堆的定义下:具有n个元素的序列 (h1,h2,...,hn),当且仅当满足(hi>=h2i,hi>=2i+1)或(hi<=h2i,hi<=2i+1) (i=1,2,...,n/2)时称之为堆。在这里只讨论满足前者条件的堆。由堆的定义可以看出,堆顶元素(即第一个元素)必为最大项(大顶堆)。完全二 叉树可以很直观地表示堆的结构。堆顶为根,其它为左子树、右子树。
算法描述(建堆,交换重新建堆):
- 初始时把要排序的数的序列看作是一棵顺序存储的二叉树,调整它们的存储序,使之成为一个 堆,这时堆的根节点的数最大
- 然后将根节点与堆的最后一个节点交换。然后对前面(n-1)个数重新调整使之成为堆
- 依此类推,直到只有两个节点的堆,并对 它们作交换,最后得到有n个节点的有序序列
代码实现:
public class HeapSort { /** * 构建大顶堆 */ public static void adjustHeap(int[] a, int i, int len) { int temp, j; temp = a[i]; for (j = 2 * i; j < len; j *= 2) {// 沿关键字较大的孩子结点向下筛选 if (j < len && a[j] < a[j + 1]) ++j; // j为关键字中较大记录的下标 if (temp >= a[j]) break; a[i] = a[j]; i = j; } a[i] = temp; } public static void heapSort(int[] a) { int i; for (i = a.length / 2 - 1; i >= 0; i--) {// 构建一个大顶堆 adjustHeap(a, i, a.length - 1); } for (i = a.length - 1; i >= 0; i--) {// 将堆顶记录和当前未经排序子序列的最后一个记录交换 int temp = a[0]; a[0] = a[i]; a[i] = temp; adjustHeap(a, 0, i - 1);// 将a中前i-1个记录重新调整为大顶堆 } } public static void main(String[] args) { int a[] = { 51, 46, 20, 18, 65, 97, 82, 30, 77, 50 }; heapSort(a); System.out.println(Arrays.toString(a)); } }
(2)直接选择排序
首先在所有记录中选出关键字值最小的记录,把它与第一个记录进行位置交换,然后在其余的记录中再选出关键字值次小的记录与第二个记录进行位置交换,依此类推,直到所有记录排好序。
算法描述:
- 置i为1
- 当
i<n
,重复下列步骤
a. 在(R[i],…,R[n])中选出一个关键字值最小的记录R[k]
b. 若R[k]不是R[i],(即k!=i),交换R[i],R[k]的位置,否则不进行交换
c. i 值加1
代码实现:
public static <T extends Comparable<T>> void selectSort(T[] arr) { for (int i = 0; i < arr.length - 1; i++) { int minIndex = i; for (int j = i + 1; j < arr.length; j++) { if (arr[j].compareTo(arr[minIndex]) < 0) { minIndex = j; } } if(minIndex != i) { swap(arr, minIndex, i); } } }
4、归并排序(二路)
归并排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
分两路,左路一直分割归并,右路一直分割归并,最后归并
算法描述:
代码实现:
public static <T extends Comparable<? super T>> void mergeSort(T[] a) { T[] tmpArray = (T[]) new Comparable[a.length]; mergeSort(a, tmpArray, 0, a.length - 1); }
//分路归并 private static <T extends Comparable<? super T>> void mergeSort(T[] a, T[] tmpArray, int left, int right) { if (left < right) { int center = (left + right) / 2; mergeSort(a, tmpArray, left, center); mergeSort(a, tmpArray, center + 1, right); merge(a,tmpArray,left,center+1,right); } }
//排序 private static <T extends Comparable<? super T>> void merge(T [] a,T [] tmpArray,int leftPos,int rightPos,int rightEnd){ int leftEnd =rightPos-1; int tmpPos = leftPos; int numElements = rightEnd - leftPos+1; while(leftPos<=leftEnd&&rightPos<=rightEnd){ if(a[leftPos].compareTo(a[rightPos])<=0) tmpArray[tmpPos++]=a[leftPos++]; else tmpArray[tmpPos++]=a[rightPos++]; } while(leftPos<=leftEnd){ tmpArray[tmpPos++]=a[leftPos++]; } while(rightPos<=rightEnd){ tmpArray[tmpPos++]=a[rightPos++]; } for(int i =0;i<numElements;i++,rightEnd--) a[rightEnd]=tmpArray[rightEnd]; }
六、排序的应用
1、如何从100万个数中找出最大的前100个数
(1)快速排序算法:
- 递归对所有数据分成[a,b)b(b,d]两个区间,(b,d]区间内的数都是大于[a,b)区间内的数
- 对(b,d]重复(1)操作,直到最右边的区间个数小于100个。注意[a,b)区间不用划分
- 返回上一个区间,并返回此区间的数字数目。接着方法仍然是对上一区间的左边进行划分,分为[a2,b2)b2(b2,d2]两个区间,取(b2,d2]区间。如果个数不够,继续(3)操作,如果个数超过100的就重复1操作,直到最后右边只有100个数为止。
(2)堆排序算法:
.先取出前100个数,维护一个100个数的最小堆,遍历一遍剩余的元素,在此过程中维护堆就可以了。
- 取前m个元素(例如m=100),建立一个小顶堆
- :顺序读取后续元素,直到结束。每次读取一个元素,如果该元素比堆顶元素小,直接丢弃
如果大于堆顶元素,则用该元素替换堆顶元素,然后保持最小堆性质。 - 最后这个堆中的元素就是前最大的100个