今天来总结一下常用的内部排序算法。内部排序算法们需要掌握的知识点大概有:算法的原理,算法的编码实现,算法的时空复杂度的计算和记忆,何时出现最差时间复杂度,以及是否稳定,何时不稳定。
首先来总结下常用内部排序算法:
类别 | 名称 | 时间复杂度(默认最坏情况) | 空间复杂度 | 稳定性 | 备注 |
插入排序 | 直接插入排序 | O(n^2) | O(1) | 稳定 | |
插入排序 | 希尔排序 | 最坏O(n^2),平均O(n^1.3) | O(1) | 不稳定 | |
交换排序 | 冒泡排序 | O(n^2) | O(1) | 稳定 | |
交换排序 | 快速排序 | 最坏O(n^2),平均O(nlogn) | 最坏O(n),平均O(logn) | 不稳定 | |
选择排序 | 简单选择排序 | O(n^2) | O(1) | 不稳定 | 简单方法中唯一一个不稳定 |
一、基础知识温习:
在进行具体算法的总结前,我们先来温习一下一些基本的概念和方法:
算法的稳定性:如果排序后,两个拥有相等关键字的元素a和b的相对位置没有发生变换,则稳定,否则不稳定。
内部排序是指在排序期间元素全部存放在内存中的排序;外部排序是指在排序期间元素无法全部同时存放在内存中,必须在排序过程中根据要求不断地在内、外存之间移动的操作。
然后再来温习一下时间复杂度的计算:
时间复杂度的计算其实就是计算出算法某条执行语句的具体执行频度,之后取频度的数量级即可。下面用例子温习:
例1:
void fun(int n){ ; ; }
设语句 i*=2运行t次,则,$t=max_t t, s.t. 2^t<n$,即t是使2^t<n成立最大的t。故$t=log_2 n(向下取整)$。故时间复杂度O(log2 n)。
例2:
count=; ;k<=n;k*=) ;j<=n;j++) count++;
外循环t次,2^t=n,t=log2 n。内循环n次。故O(nlog2n)。
例3:
y=; )(y+)<=n) y+=;
$t=max_t t , s.t. t^2<=n$t=n^(1/2)。O(n^0.5)。
例4:
;i<=n;i++) ;j<=i;j++) ;k<=j;k++) x++;
内层循环受外循环控制变量大小控制,故不能简单相乘。研究发现,i=某值c时,最内层循环次数为:1+2+…+c。故$O(\sum_i^n i*(n-i-1))$但不会算,不过有个结论可以用:$\sum_{i=1}^n\sum_{j=1}^i\sum_{k=1}^j l=O(\frac{n^3}{6})=O(n^3)$
另外总结一些常用数学公式:
等差数列求和公式:$Sn = \frac{a_1+a_n}{2}n=a_1n+\frac{n(n-1)}{2}d$
等比数列求和公式:$Sn = a_1\frac{1-q^n}{1-q}$
和公式:$1+2+...+n=\frac{n(n+1)}{2}$
平方和公式:$1^2+2^2+...+n^2=\frac{n(n+1)(2n+1)}{6}$
立方和公式:$1^3+2^3+...+n^3=\frac{n^2(n+1)^2}{4}=(\frac{n(n+1)}{2})^2$
二、排序算法们:
2.1直接插入排序:
直接插入排序的思想是,在序列前一部分有序时,将后一部分最前面的元素插入到有序部分,使插入后的序列依然有序。(找位置,找位置过程中移动元素腾出位置,然后插入)具体的做法(文字描述就省略啦,自己看代码吧):
public static void directInsertSort(int[] a) { int len = a.length; int temp; for(int i=1;i<len;i++) { //从第二个元素开始,因为一个元素时必然是有序的 if(a[i]<a[i-1]) { temp = a[i]; int j=i-1; for(;j>=0;j--) { if(a[j]>temp) a[j+1]=a[j]; //不用>=是为了保证算法的稳定性,不然后出现的元素就会排在前出现的元素之前了,而且那样也会增加无谓的开销 } a[j+1]=temp; } } }
最差时间复杂度:应该出现在每次内循环中,都要找到最前面的位置插入,即,原数组是倒序的。这时运行频次t=0+1+2+...+(n-1)=n(n-1)/2。故O(n^2)。空间开销为常量们,O(1)。
2.2冒泡排序(基本交换排序):
冒泡排序的思想是,每次将最小的元素交换到队首。(对比左右元素,只要右元素小,就交换位置)。具体算法:
public static void bubbleSort(int[] a) { int len = a.length; int temp; for(int i=0;i<len;i++) { boolean flag=false; //表示本躺冒泡是否发生交换 for(int j=len-1;j>i;j--) { if(a[j]<a[j-1]) { //稳定 temp=a[j]; a[j]=a[j-1]; a[j-1]=temp; flag=true; } } if(flag==false) return; //已经有序了,避免后续无用功 } }
最差时间复杂度:倒序。同上O(n^2),O(1)。
2.3简单选择排序:
简单选择排序的思想是,每轮找到一个最小的数,和无序部分的第一个元素交换位置。(遍历后交换位置,不存在略过的元素的移动)。具体代码:
public static void selectionSort(int[] a) { int len=a.length; int temp; for(int i=0;i<len;i++) { int min = i; for(int j=i+1;j<len;j++) { if(a[j]<a[min]) min=j; } if(min!=i) { temp=a[i]; a[i]=a[min]; a[min]=temp; } } }
首先由于交换位置的原因,那么第一个最小元素(出现在靠后的位置)就会跑到前面,故不稳定。
时间复杂度方面相同,O(n^2),O(1)。
2.4希尔排序:
希尔排序是升级版的直接插入排序,它引入了步长的概念,先对等步长的每个子序列进行直接插入排序,然后缩短步长,最后步长=1,进行最后一次排序。
public static void shellSort(int[] a) { int len = a.length; int temp; for(int dk=len/2;dk>=1;dk/=2) { //步长变化 for(int i=dk;i<=len;i++) { if(a[i]<a[i-dk]) {//如果需要插入 temp=a[i]; int j=i-dk; for(;j>=0;j-=dk) { if(a[j]>temp) {a[j+dk]=a[j];} } a[j+dk]=temp; } } } }
希尔排序的时间复杂度不好计算,直接记结论,最坏O(n^2),平均O(n^1.3)。当相同关键字的元素被分到不同的子表时,顺序可能会发生变化,故不稳定。
2.5快速排序:
著名的快排,是交换排序的一种,思想是每轮把一个数放到它该在的位置(左边的元素都比它小,右边都比它大),即左右各一个指针,先从右边往左找,找到第一个比pivot(基准)小的值,停下,左指针往右找第一个比pivot大的元素,停下,交换两个元素,之后继续,直到两个指针相遇,右指针的位置的元素应小于pivot的值(右指针先走的原因),即为pivot应在的位置,右指针元素与pivot交换即可。还不明白可以参考http://wiki.jikexueyuan.com/project/easy-learn-algorithm/fast-sort.html的介绍。具体:
public static void quickSort(int[] a) { forQuickSort(a,0,a.length-1); } public static void forQuickSort(int[] a,int l,int r) { if(l==r) return; int temp; int pr=r,pl=l; //工作指针,先从右边开始 while(pr>pl) { while(pr>pl&&a[pr]>=a[l]) pr--; while(pr>pl&&a[pl]<=a[l]) pl++; if(pr==pl) {temp=a[l];a[l]=a[pr];a[pr]=temp;} else {temp=a[pr];a[pr]=a[pl];a[pl]=temp;} } forQuickSort(a,l,pr-1); forQuickSort(a,pr+1,pr); }
还有一种写法:
private int Partition(int [] input,int start,int end){ int p = input[start]; int left = start+1,right=end; while(left<=right){ while(left<=right&&input[right]>=p) right--; while(left<=right&&input[left]<=p) left++; if(left<right){int temp=input[right];input[right]=input[left];input[left]=temp;} else{input[start]=input[right];input[right]=p;} } return right; }
当元素基本有序或者逆序时,快排会得到最坏时间复杂度,O(n^2)。如果比较顺利,快排两边比较均衡,则运行速度将大大提升,达到O(nlogn),而快排的平均时间复杂度和最佳表现接近,也是O(nlogn)。快排是内部排序算法中平均性能最优的排序算法。
对于空间复杂度,快排是递归的,故需要一个递归工作栈,最坏O(n),平均O(logn)。
因为存在左右指针相互交换的情况,所以快排是不稳定的。
关于对多个有序数组合并排序,看mergesort,时间复杂度O(m+n)
https://www.cnblogs.com/kkun/archive/2011/11/23/merge_sort.html
https://www.cnblogs.com/chengxiao/p/6194356.html
关于快速排序时间复杂度证明:
https://www.cnblogs.com/fengty90/p/3768827.html