初级排序方法有直接插入排序、直接交换排序(冒泡)、直接选择排序。对应的高级排序方法分别是shell排序、快速排序、堆排序
另外高级排序算法还有归并排序。
下面演示对数组R[]进行非降排序:
直接插入排序:
public static int[] InsertSort(int[] R){ int n =R.length; for(int j =1;j<n;j++){ int i= j-1; int K = R[j]; while(i>=0&&K < R[i]){ R[i+1]=R[i]; i--; } R[i+1] = K; } return R; }
如果将数组的第一个元素置为一个最小值,那么算法中的 while(i>=0&&K<R[i]){……} 可以改写成 while(K<R[i]){……} ;这样就可以节省比较 i 的时间。
直接插入排序(冒泡排序):
每一趟排序能够找出一个最大值,并把它交换到最终位置。因此每一趟的扫描次数可以减 1 ;
public static int[] BoubleSort(int[] R){ int n = R.length; for(int j =n-1;j>=1;j--){ for(int i =1;i<=j;i++){ if(R[i]<R[i-1]){ int t =R[i]; R[i] = R[i-1]; R[i-1] = t; } } } return R; }
冒泡排序还可以通过添加标记的方法减少不必要的比较次数,如下代码用 Bound 控制每趟扫描的边界,这样就可以加速算法。
public static int[] BSort(int[] R){ int n = R.length; int j = n; int Bound = n; while(Bound!=0){ Bound = j; j = 0; for(int i =1;i<Bound;i++){ if(R[i]<R[i-1]){ int t =R[i]; R[i] = R[i-1]; R[i-1] = t; j = i; } } } return R; }
直接选择排序:
直接选择排序每次从待排序列中选出一个最小值(用一个数m来记录最小值的下标,提高算法效率)放在它的最终位置,每次选最小元素的时间复杂度是 O(n) ,有 n 个元素,n*O(n) -> 算法的效率是 O(n*n).
public static int[] SelectSort(int[] R){ int n =R.length; for(int j =0;j<n;j++){ int K = R[j]; int m = j; int i = j+1; while(i<n){ if(R[i]<K){ K = R[i]; m =i; } i++; } int t = R[j]; R[j] = R[m]; R[m] = t; } return R; }
高级排序算法:
shell 排序:
shell 排序是目前对小规模数据(少于 50 个元素)进行排序的最有效的算法(优于快速排序),它的主要思想是通过逐减(由大到小)增量对数据进行分组,对每组数据单独使用直接插入排序算法进行组内排序;当增量减小到 1 时,所有的数据变成一组,经过最后一趟直接插入排序后得到有序数列。 shell 排序之所以效率很高,主要是因为当增量很大时,组内一次交换减少的乱序对数量相当可观,因而 shell 排序是很有效的排序算法。
复杂度: shell 排序的时间复杂度与其所选取的增量关系密切,目前已知最好的序列是{1,4,10,23,57,132,301,701,1750……},当渐减增量序列形如 2^p*3^q 时shell排序时间复杂度是 O(n*(log(n))^2) ;
下面我以 n/(2^i),i =1,2,3……为渐减序列进行 shell 排序:
public class ShellSort{ static int[] a = {0,2,1,8,3,46,85,14,85,78,56,4,5,69,8755,1,12,54,15,5,}; public static void main(String[] args){ int n = a.length; show(a); shell(a); System.out.println("shell排序:"); show(a); } public static void shell(int[] R){ //int[] s = {1,4,10,23,57,132,301,701,1750};//目前已知的最好的增量序列 int n = R.length; int h = n; while(h != 1){ for(int i =0;i<h;i++){ h = h/2; InsertSort(R,i,n-1,h); } } } public static void InsertSort(int[] R,int a,int b,int h){ for(int j = a+h;j<=b;j+=h){ int i =j-h; int K = R[j]; while(i>=a&&K<R[i]){ R[i+h]=R[i]; i-=h; } R[i+h] = K; } } public static void show(int[] a ){ for(int i =0;i<a.length;i++){ System.out.print(a[i]+" "); } System.out.println(); } }
快速排序:
快速排序是一种效率特别高的排序算法,主要思想是首先确定一个基准 R[m] (通常选用第一个元素,当待排序列基本有序时,总是选第一个数作为基准会使算法效率特别低,这时就需要使用三者取中法把R[m],R[n],R[(m+n)/2]中的中间元素交换到R[0]处作为基准);
(1)算法开始时首先初始化两个指针i,j,一个指向基准元素(加 1 操作后指向基准元素的下一个元素),一个指向最末元素下一个(减 1 操作后指向最末元素);
(2)然后第一个指针 ( i ) 往后移寻找大于基准的元素找到后停止移动,第二个指针(j)从后往前寻找小于基准的元素找到后停止移动;
(3)当 i,j 未发生交叉时,R[i]<--> R[j];
(4)当 i,j 有交叉时 基准<-->R[j],本趟排序结束,基准交换到它的最终位置(可以根据这一性质设计复杂度为 O(n) 的查找低 i 大元素的算法);
(5)分别对 j 前后的序列进行下一趟快速排序。
class QSort{//排序结果为非降 static int[] a = {0,2,1,8,3,46,85,14,85,78,56,4,5,69,8755,1,12,54,15,5,}; public static void main(String[] args){ //快速排序 int n = a.length; show(a); quickSort(a,0,n-1); System.out.println("快速排序"); show(a); } public static void quickSort(int[] R,int a,int b){//R[a]~R[b] if(b-a<1){ return; } if(b-a==1){ if(R[a]>R[b]){ int t = R[a]; R[a] = R[b]; R[b] = t; } return; } int n = b; int m = a; int mid = (m+n)/2; int t; if(R[n]<R[mid]){ t = R[n]; R[n] = R[mid]; R[mid] = t; } if(R[n]<R[m]){ t = R[n]; R[n] = R[m]; R[m] = t; } if(R[m]<R[mid]){ t = R[m]; R[m] = R[mid]; R[mid] =t; }//三者取中,中值存在R[m]中 int i =m; int j =n+1; while(i<j){ i++; while(R[i]<R[m]){ i++; } j--; while(R[j]>R[m]){ j--; } if(i<j){ t = R[i]; R[i] = R[j]; R[j] = t; } } t = R[j]; R[j] = R[m]; R[m] = t; quickSort(R,m,j-1); quickSort(R,j+1,n); return; } public static void show(int[] a ){ for(int i =0;i<a.length;i++){ System.out.print(a[i]+" "); } System.out.println(); } }
堆排序:
堆排序包含两个过程,第一个过程是初始建堆(由下向上);第二个过程是堆排序(自上而下),把堆顶与逐末元素交换,再重建堆。
上面两个过程都需要使用算法 Restore(R,f,e) 来调整堆中的元素,每次调整时父节点与儿子结点比较,如果儿子结点比他大就把比较大的儿子结点与父节点互换位置,然后重复此操作直至堆底。
public class HeapSort{ static int[] a = {0,2,1,8,3,46,85,14,85,78,56,4,5,69,8755,1,12,54,15,5,}; public static void main(String[] args){ HeapSort(a); System.out.println("堆排序:"); show(a); } public static void HeapSort(int[] R){ int n = a.length; for(int i =n-1;i>=0;i-- ){//重建堆:从下向上 Restore(a,i,n-1); } for(int i =n-1;i>0;i--){//堆排序:从上向下 Restore(a,0,i); int t = a[0]; a[0] = a[i]; a[i] = t; } } public static void Restore(int[] R,int f,int e){ int j =f; int m; while(2*j+1<e){ if(2*j+2<e&&R[2*j+1]<R[2*j+2]){ m = 2*j+2; }else{ m = 2*j+1; } if(R[j]<R[m]){ int t = R[j]; R[j] = R[m]; R[m] = t; j = m; }else{ j = e;//跳出循环 } } } public static void show(int[] a ){ for(int i =0;i<a.length;i++){ System.out.print(a[i]+" "); } System.out.println(); } }
归并排序:
归并排序与快速排序一样都利用了分治思想,归并排序把排序的问题转化为了合并两个数组的问题,简单易懂。算法的复杂度主要由归并的趟数和数据的规模决定。归并的趟数近视等于 log2(n) (把归并的过程看成一个树结构,树的高度近似是 log2(n) ),每一趟关键词比较次数复杂度为 O(n) ;整个过程的时间复杂度是 n*log(n) 。
public class MSort{ static int[] a = {0,2,1,8,3,46,85,14,85,78,56,4,5,69,8755,1,12,54,15,5,}; public static void main(String[] args){ show(a); a = MSort(a); System.out.println("MSort:"); show(a); } public static int[] MSort(int[] R){ int n =R.length; int[] X = new int[n]; int length = 1; while(length<n){ int i =0; while(i+2*length-1<n){ Merge(R,i,i+length-1,i+2*length-1,X); i = i+2*length; } if(i+length-1<n-1){ Merge(R,i,i+length-1,n-1,X); } length = 2*length; int[] t = X; X = R; R = t; } return R; } public static void Merge(int[] R,int a,int b,int c,int[] X){ int i = a; int j = b+1; int k = a; while(k<=c){ while(i<=b&&j<=c){ if(R[j]<R[i]){ X[k] = R[j]; j++; }else{ X[k] = R[i]; i++; } k++; } while(i<=b){ X[k] = R[i]; i++; k++; } while(j<=c){ X[k] = R[j]; j++; k++; } } } public static void show(int[] a ){ for(int i =0;i<a.length;i++){ System.out.print(a[i]+" "); } System.out.println(); } }
总结:
时间复杂度 : 所有的简单排序算法再加上直接插入排序的改进算法(shell排序)都是平方阶简单排序算法:O(n^2),shell排序:O(n*(logn)^2);
高级排序算法中的快速排序,合并排序,堆排序的时间复杂度都是线性对数阶 O(n*logn);
具体的时空复杂度差异:快速排序 < 归并排序 < 堆排序 . 快速排序当值无愧是性能冠军。
稳定性:高级算法中除归并算法外都不稳定;初级排序算法中除直接选择排序外都是稳定的排序算法。