数据结构---九大排序算法再总结

排序:对一序列对象根据某个关键字进行排序;


稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;

不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;

 

内排序:所有排序操作都在内存中完成;

外排序:由于数据太大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;

 

排序耗时的操作:比较、移动;

排序分类:

(1)交换类:冒泡排序、快速排序;此类的特点是通过不断的比较和交换进行排序;

(2)插入类:简单插入排序、希尔排序;此类的特点是通过插入的手段进行排序;

(3)选择类:简单选择排序、堆排序;此类的特点是看准了再移动;

(4)归并类:归并排序;此类的特点是先分割后合并;

 

历史进程:一开始排序算法的复杂度都在O(n^2),希尔排序的出现打破了这个僵局;

 

以下视频是Sapientia University创作的,用跳舞的形式演示排序步骤,这些视频就可以当作复习排序的资料~

冒泡排序视频:http://v.youku.com/v_show/id_XMzMyOTAyMzQ0.html

选择排序视频:http://v.youku.com/v_show/id_XMzMyODk5MDI0.html

插入排序视频:http://v.youku.com/v_show/id_XMzMyODk3NjI4.html

希尔排序视频:http://v.youku.com/v_show/id_XMzMyODk5MzI4.html

归并排序视频:http://v.youku.com/v_show/id_XMzMyODk5Njg4.html

快速排序视频:http://v.youku.com/v_show/id_XMzMyODk4NTQ4.html

 


上面介绍的排序算法都是基于排序的,还有一类算法不是基于比较的排序算法,即计数排序、基数排序


预备:最简单的排序


此种实现方法是最简单的排序实现;

缺点是每次找最小值都是单纯的找,而没有为下一次寻找做出铺垫;

算法如下:

 

[java]  view plain  copy
  1. public static int[] simple_sort(int[] arr) {  
  2.     for (int i = 0; i < arr.length; i++) {  
  3.         for (int j = i + 1; j < arr.length; j++) {  
  4.             if (arr[i] > arr[j]) {  
  5.                 swap(arr, i, j);  
  6.             }  
  7.         }  
  8.     }  
  9.     return arr;  
  10. }  


一、冒泡排序


冒泡排序相对于最简单的排序有了改进,即每次交换都是对后续有帮助的,大数将会越来越大,小的数将会越来越小;

冒泡排序思想:两两相邻元素之间的比较,如果前者大于后者,则交换

因此此排序属于交换排序一类,同类的还有现在最常用的排序方法:快速排序;


1.标准冒泡排序


此种方法是最一般的冒泡排序实现,思想就是两两相邻比较并交换;

算法实现如下:

[java]  view plain  copy
  1. public static int[] bubble_sort2(int[] arr) {  
  2.     for (int i = 0; i < arr.length; i++) {  
  3.         for (int j = arr.length - 1; j > i; j--) {  
  4.             if (arr[j] < arr[j - 1]) {  
  5.                 swap(arr, j, j - 1);  
  6.             }  
  7.         }  
  8.     }  
  9.     return arr;  
  10. }  


2.改进冒泡排序


改进在于如果出现一个序列,此序列基本是排好序的,如果是标准的冒泡排序,则还是需要进行不断的比较;

改进方法:通过一个boolean isChanged,如果一次循环中没有交换过元素,则说明已经排好序;

算法实现如下:

 

[java]  view plain  copy
  1. // 最好:n-1次比较,不移动,因此时间复杂度为O(n),不占用辅助空间  
  2. // 最坏:n(n-1)/2次比较和移动,因此O(n^2),占用交换的临时空间,大小为1;  
  3. public static int[] bubble_sort3(int[] arr) {  
  4.     boolean isChanged = true;  
  5.     for (int i = 0; i < arr.length && isChanged; i++) {  
  6.         isChanged = false;  
  7.         for (int j = i + 1; j < arr.length; j++) {  
  8.             if (arr[i] > arr[j]) {  
  9.                 swap(arr, i, j);  
  10.                 isChanged = true;  
  11.             }  
  12.         }  
  13.     }  
  14.     return arr;  
  15. }  


二、简单选择排序

简单选择排序特点:每次循环找到最小值,并交换,因此交换次数始终为n-1次;

相对于最简单的排序,对于很多不必要的交换做了改进,每个循环不断比较后记录最小值,只做了一次交换(当然也可能不交换,当最小值已经在正确位置)

算法如下:

 

[java]  view plain  copy
  1. //最差:n(n-1)/2次比较,n-1次交换,因此时间复杂度为O(n^2)  
  2. //最好:n(n-1)/2次比较,不交换,因此时间复杂度为O(n^2)  
  3. //好于冒泡排序  
  4. public static int[] selection_sort(int[] arr) {  
  5.     for (int i = 0; i < arr.length - 1; i++) {  
  6.         int min = i;  
  7.         for (int j = i + 1; j < arr.length; j++) {  
  8.             if (arr[min] > arr[j]) {  
  9.                 min = j;  
  10.             }  
  11.         }  
  12.         if (min != i)  
  13.             swap(arr, min, i);  
  14.     }  
  15.     return arr;  
  16. }  

 

三、简单插入排序


思想: 给定序列,存在一个分界线,分界线的左边被认为是有序的,分界线的右边还没被排序,每次取没被排序的最左边一个和已排序的做比较,并插入到正确位置;我们默认索引0的子数组有序;每次循环将分界线右边的一个元素插入有序数组中,并将分界线向右移一位;

算法如下:

 

[java]  view plain  copy
  1. // 最好:n-1次比较,0次移动 ,时间复杂度为O(n)  
  2. // 最差:(n+2)(n-1)/2次比较,(n+4)(n-1)/2次移动,时间复杂度为 O(n^2)  
  3. public static int[] insertion_sort(int[] arr) {  
  4.     int j;  
  5.     for (int i = 1; i < arr.length; i++) {  
  6.         if (arr[i] < arr[i - 1]) {  
  7.             int tmp = arr[i];  
  8.             for (j = i - 1; j >= 0 && arr[j] > tmp; j--) {  
  9.                 arr[j + 1] = arr[j];  
  10.             }  
  11.             arr[j + 1] = tmp;  
  12.         }  
  13.     }  
  14.     return arr;  
  15. }  


简单插入排序比选择排序和冒泡排序好!


 

四、希尔排序


1959年Shell发明;

第一个突破O(n^2)的排序算法;是简单插入排序的改进版;

思想:由于简单插入排序对于记录较少或基本有序时很有效,因此我们可以通过将序列进行分组排序使得每组容量变小,再进行分组排序,然后进行一次简单插入排序即可;

这里的分组是跳跃分组,即第1,4,7位置为一组,第2,5,8位置为一组,第3,6,9位置为一组;


索引

1

2

3

4

5

6

7

8

9


此时,如果increment=3,则i%3相等的索引为一组,比如索引1,1+3,1+3*2

一般增量公式为:increment = increment/3+1;

算法实现如下:

 

[java]  view plain  copy
  1. // O(n^(3/2))  
  2. //不稳定排序算法  
  3. public static int[] shell_sort(int[] arr) {  
  4.     int j;  
  5.     int increment = arr.length;  
  6.     do {  
  7.         increment = increment / 3 + 1;  
  8.         for (int i = increment; i < arr.length; i++) { //i=increment 因为插入排序默认每组的第一个记录都是已排序的  
  9.             if (arr[i] < arr[i - increment]) {  
  10.                 int tmp = arr[i];  
  11.                 for (j = i - increment; j >= 0 && arr[j] > tmp; j -= increment) {  
  12.                     arr[j + increment] = arr[j];  
  13.                 }  
  14.                 arr[j + increment] = tmp;  
  15.             }  
  16.         }  
  17.     } while (increment > 1);  
  18.     return arr;  
  19. }  

五、堆排序

 

Floyd和Williams在1964年发明;

大根堆:任意父节点都比子节点大;

小根堆:任意父节点都比子节点小;


不稳定排序算法,是简单选择排序的改进版;

思想:构建一棵完全二叉树,首先构建大根堆,然后每次都把根节点即最大值移除,并用编号最后的节点替代,这时数组长度减一,然后重新构建大根堆,以此类推;

注意:此排序方法不适用于个数少的序列,因为初始构建堆需要时间;

算法实现如下:

 

[java]  view plain  copy
  1.        // 时间复杂度为O(nlogn)   
  2. //不稳定排序算法  
  3. //辅助空间为1  
  4. //不适合排序个数较少的序列  
  5. public static int[] heap_sort(int[] arr) {  
  6.     int tmp[] = new int[arr.length + 1];  
  7.     tmp[0] = -1;  
  8.     for (int i = 0; i < arr.length; i++) {  
  9.         tmp[i + 1] = arr[i];  
  10.     }  
  11.     // 构建大根堆:O(n)  
  12.     for (int i = arr.length / 2; i >= 1; i--) {  
  13.         makeMaxRootHeap(tmp, i, arr.length);  
  14.     }  
  15.     // 重建:O(nlogn)  
  16.     for (int i = arr.length; i > 1; i--) {  
  17.         swap(tmp, 1, i);  
  18.         makeMaxRootHeap(tmp, 1, i - 1);  
  19.     }  
  20.     for (int i = 1; i < tmp.length; i++) {  
  21.         arr[i - 1] = tmp[i];  
  22.     }  
  23.     return arr;  
  24. }  
  25.   
  26. private static void makeMaxRootHeap(int[] arr, int low, int high) {  
  27.     int tmp = arr[low];  
  28.     int j;  
  29.     for (j = 2 * low; j <= high; j*=2) {  
  30.         if (j < high && arr[j] < arr[j + 1]) {  
  31.             j++;  
  32.         }  
  33.         if (tmp >= arr[j]) {  
  34.             break;  
  35.         }  
  36.         arr[low] = arr[j];  
  37.         low = j;  
  38.     }  
  39.     arr[low] = tmp;  
  40. }  

六、归并排序


稳定排序算法;

思想:利用递归进行分割和合并,分割直到长度为1为止,并在合并前保证两序列原本各自有序,合并后也有序;

实现代码如下:

 

[java]  view plain  copy
  1. // 稳定排序;  
  2. // 时间复杂度O(nlogn)  
  3. // 空间复杂度:O(n+logn)  
  4. public static int[] merge_sort(int[] arr) {  
  5.     Msort(arr, arr, 0, arr.length - 1);  
  6.     return arr;  
  7. }  
  8.   
  9. private static void Msort(int[] sr, int[] tr, int s, int t) {  
  10.     int tr2[] = new int[sr.length];  
  11.     int m;  
  12.     if (s == t) {  
  13.         tr[s] = sr[s];  
  14.     } else {  
  15.         m = (s + t) / 2;  
  16.         Msort(sr, tr2, s, m);  
  17.         Msort(sr, tr2, m + 1, t);  
  18.         Merge(tr2, tr, s, m, t);  
  19.     }  
  20. }  
  21.   
  22. private static void Merge(int[] tr2, int[] tr, int i, int m, int t) {  
  23.     int j, k;  
  24.     for (j = i, k = m + 1; i <= m && k <= t; j++) {  
  25.         if (tr2[i] < tr2[k]) {  
  26.             tr[j] = tr2[i++];  
  27.         } else {  
  28.             tr[j] = tr2[k++];  
  29.         }  
  30.     }  
  31.     while (i <= m) {  
  32.         tr[j++] = tr2[i++];  
  33.     }  
  34.     while (k <= t) {  
  35.         tr[j++] = tr2[k++];  
  36.     }  
  37. }  

七、快速排序


冒泡排序的升级版;现在用的最多的排序方法;

思想:选取pivot,将pivot调整到一个合理的位置,使得左边全部小于他,右边全部大于他;

注意:如果序列基本有序或序列个数较少,则可以采用简单插入排序,因为快速排序对于这些情况效率不高;

实现代码如下:

[java]  view plain  copy
  1.        // 不稳定排序算法  
  2. // 时间复杂度:最好:O(nlogn) 最坏:O(n^2)  
  3. // 空间复杂度:O(logn)  
  4. public static int[] quick_sort(int[] arr) {  
  5.     qsort(arr, 0, arr.length - 1);  
  6.     return arr;  
  7. }  
  8.   
  9. private static void qsort(int[] arr, int low, int high) {  
  10.     int pivot;  
  11.     if (low < high) {  
  12.         pivot = partition(arr, low, high);  
  13.         qsort(arr, low, pivot);  
  14.         qsort(arr, pivot + 1, high);  
  15.     }  
  16. }  
  17.   
  18. private static int partition(int[] arr, int low, int high) {  
  19.     int pivotkey;  
  20.     pivotkey = arr[low];//选择pivot,此处可以优化  
  21.     while (low < high) {  
  22.         while (low < high && arr[high] >= pivotkey) {  
  23.             high--;  
  24.         }  
  25.         swap(arr, low, high);//交换,此处可以优化  
  26.         while (low < high && arr[low] <= pivotkey) {  
  27.             low++;  
  28.         }  
  29.         swap(arr, low, high);  
  30.     }  
  31.     return low;  
  32. }  


优化方案


(1)选取pivot:选取pivot的值对于快速排序至关重要,理想情况,pivot应该是序列的中间数;

而前面我们只是简单的取第一个数作为pivot,这点可以进行优化;

优化方法:抽多个数后取中位数作为pivot;

(2)对于小数组使用插入排序:因为快速排序适合大数组排序,如果是小数组,则效果可能没有简单插入排序来得好;


如果想进行优化,则可以使用以下代码:

[java]  view plain  copy
  1. public static int[] quick_sort(int[] arr) {  
  2.     if(arr.length>10){  
  3.         qsort(arr, 0, arr.length - 1);  
  4.     }  
  5.     else{  
  6.         insertion_sort(arr);  
  7.     }  
  8.     return arr;  
  9. }  


八、计数排序


计数排序是典型的不是基于比较的排序算法,基于比较的排序算法最少也要O(nlogn),有没有可能创造线性时间的排序算法呢?那就是不基于比较的排序算法;

如果数组的数据范围为0~100,则很适合此算法;

复杂度: O(n+k), n为原数组长度,k为数据范围;


思想:


(1)首先找出数组中的最大值,然后创建一个计数数组(用来记录每个元素的数量),长度为max,比如数组为{1,1,2,3,4,5},则创建一个长度为6的数组count[],count[1]存放数值1出现的次数,即2;

(2)填充count数组,即遍历原数组,并且count[arr[i]-1]++;

(3)对count数组进行累加,即count[i] = count[i] + count[i-1];

(4)反向填充result数组,result[count[arr[i]]-1] = arr[i];


代码如下:

[java]  view plain  copy
  1. import java.util.ArrayList;  
  2. import java.util.Scanner;  
  3.   
  4.   
  5. /** 
  6.  * 计数排序适用于: 
  7.  *  (1)数据范围较小,建议在小于1000 
  8.  *  (2)每个数值都要大于等于0 
  9.  * @author xiazdong 
  10.  * 
  11.  */  
  12. public class Count_Sort {  
  13.       
  14.     public static void main(String[] args) {  
  15.         int[] array = readArray();  
  16.         System.out.print("排序前数组为:");  
  17.         print(array);  
  18.         int result[] = count_sort(array);  
  19.         System.out.print("排序后数组为:");  
  20.         print(result);  
  21.     }  
  22.   
  23.     //读取数组函数  
  24.     private static int[] readArray() {  
  25.         Scanner in = new Scanner(System.in);  
  26.         ArrayList<Integer> list = new ArrayList<Integer>();  
  27.         while(true){  
  28.             System.out.print("输入数字:");  
  29.             int element = in.nextInt();  
  30.             if(element==-1){  
  31.                 break;  
  32.             }  
  33.             else{  
  34.                 list.add(element);  
  35.             }  
  36.         }  
  37.         Integer[] arr = list.toArray(new Integer[0]);  
  38.         int[]array = new int[arr.length];  
  39.         for(int i=0;i<arr.length;i++){  
  40.             array[i] = arr[i];  
  41.         }  
  42.         return array;  
  43.     }  
  44.     //计数排序  
  45.     public static int[] count_sort(int arr[]){  
  46.         int gap = findGap(arr);  
  47.         int[] count = new int[gap];  
  48.         int[] result = new int[arr.length];  
  49.         for(int i=0;i<arr.length;i++){  
  50.             count[arr[i]]++;  
  51.         }  
  52.         for(int i=1;i<count.length;i++){  
  53.             count[i] = count[i] + count[i-1];  
  54.         }  
  55.         //反向填充结果数组  
  56.         for(int i=arr.length-1;i>=0;i--){  
  57.             result[count[arr[i]]-1] = arr[i];   
  58.             count[arr[i]]--;  
  59.         }  
  60.         return result;  
  61.     }  
  62.     public static void print(int result[]){  
  63.         for(int a:result){  
  64.             System.out.print(a+" ");  
  65.         }  
  66.         System.out.println();  
  67.     }  
  68.     /** 
  69.      * 找出数组的数据范围,即最大数的值 
  70.      * @param arr 
  71.      * @return 
  72.      */  
  73.     private static int findGap(int[] arr) {  
  74.         int max = arr[0];  
  75.         for(int i=1;i<arr.length;i++){  
  76.             if(max<arr[i]){  
  77.                 max = arr[i];  
  78.             }  
  79.         }  
  80.         return (max+1);  
  81.     }  
  82. }  


九、基数排序


基数排序也是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;

比如{987,789} ,先通过个位数排序:{987,789},再通过十位数排序:{987,789},再通过百位数排序:{789,987}


思想:

(1)取得数组中的最大数,并取得位数;

(2)arr为原始数组,从最低位开始取每个位组成radix数组;

(3)对radix进行计数排序(利用计数排序适用于小范围数的特点);


[java]  view plain  copy
  1. import java.util.ArrayList;  
  2. import java.util.Scanner;  
  3.   
  4.   
  5. /** 
  6.  * 计数排序适用于: 
  7.  *  (1)数据范围较小,建议在小于1000 
  8.  *  (2)每个数值都要大于等于0 
  9.  * @author xiazdong 
  10.  * 
  11.  */  
  12. public class Count_Sort {  
  13.       
  14.     public static void main(String[] args) {  
  15.         int[] array = new int[]{1046,2084,9046,12074,56,7026,8099,17059,33,1};  
  16.         System.out.print("排序前数组为:");  
  17.         print(array);  
  18.         int result[] = radix_sort(array);  
  19.         System.out.print("排序后数组为:");  
  20.         print(result);  
  21.     }  
  22.   
  23.     //基数排序 O(kn)   
  24.     public static int[] radix_sort(int[]arr){  
  25.         int radix[] = new int[arr.length];  
  26.         int count = 1;  
  27.         int n = findMaxLength(arr);  
  28.         for(int i=0;i<n;i++){  
  29.             radix = getRadix(arr,count);  
  30.             arr = count_sort(arr, radix);  
  31.             count *=10;  
  32.         }  
  33.         return arr;  
  34.     }  
  35.     private static int findMaxLength(int[] arr) {  
  36.         int max = arr[0];  
  37.         for(int i=1;i<arr.length;i++){  
  38.             if(max<arr[i]){  
  39.                 max = arr[i];  
  40.             }  
  41.         }  
  42.         int count = 1;  
  43.         int mcount = 1;  
  44.         while((max / mcount)!=0){  
  45.             mcount = 1;  
  46.             count++;  
  47.             for(int i=0;i<count;i++){  
  48.                 mcount *=10;  
  49.             }  
  50.         }  
  51.         return count;  
  52.     }  
  53.   
  54.   
  55.     //取得需要排序的位的数组  
  56.     private static int[] getRadix(int[] arr,int count) {    //O(n)  
  57.         int radix[] = new int[arr.length];  
  58.         for(int i=0;i<arr.length;i++){  
  59.             radix[i] = arr[i]/count % 10;  
  60.         }  
  61.         return radix;  
  62.     }  
  63.   
  64.     //类似计数排序  
  65.     //arr为原始数组  
  66.     //radix为需要排序的位的数组  
  67.     public static int[] count_sort(int arr[],int radix[]){  
  68.         int gap = findGap(radix);  
  69.         int[] count = new int[gap];  
  70.         int[] result = new int[radix.length];  
  71.         for(int i=0;i<radix.length;i++){  
  72.             count[radix[i]]++;  
  73.         }  
  74.         for(int i=1;i<count.length;i++){  
  75.             count[i] = count[i] + count[i-1];  
  76.         }  
  77.         //反向填充结果数组  
  78.         for(int i=radix.length-1;i>=0;i--){  
  79.             result[count[radix[i]]-1] = arr[i];   
  80.             count[radix[i]]--;  
  81.         }  
  82.         return result;  
  83.     }  
  84.     public static void print(int result[]){  
  85.         for(int a:result){  
  86.             System.out.print(a+" ");  
  87.         }  
  88.         System.out.println();  
  89.     }  
  90.     /** 
  91.      * 找出数组的数据范围,即最大数的值 
  92.      * @param arr 
  93.      * @return 
  94.      */  
  95.     private static int findGap(int[] arr) {  
  96.         int max = arr[0];  
  97.         for(int i=1;i<arr.length;i++){  
  98.             if(max<arr[i]){  
  99.                 max = arr[i];  
  100.             }  
  101.         }  
  102.         return (max+1);  
  103.     }  
  104. }  




对比图


此图摘自http://www.cnblogs.com/cj723/archive/2011/04/29/2033000.html的图








总结:每个排序都有每个排序的优点,我们需要在适当的时候用适当的算法;

比如在基本有序、数组规模小时用直接插入排序;

比如在大数组时用快速排序;

比如如果要想稳定性,则使用归并排序;



摘录维基百科图片:








当年看了《大话数据结构》总结的,但是现在看了《算法导论》,发现以前对排序的理解还不深入,所以打算对各个排序的思想再整理一遍。
本文首先介绍了基于比较模型的排序算法,即最坏复杂度都在Ω(nlgn)的排序算法,接着介绍了一些线性时间排序算法,这些排序算法虽然都在线性时间,但是都是在对输入数组有一定的约束的前提下才行。
这篇文章参看了《算法导论》第2、3、4、6、7、8章而总结。

算法的由来:9世纪波斯数学家提出的:“al-Khowarizmi”



排序的定义:
输入:n个数:a1,a2,a3,...,an
输出:n个数的排列:a1',a2',a3',...,an',使得a1'<=a2'<=a3'<=...<=an'。

In-place sort(不占用额外内存或占用常数的内存):插入排序、选择排序、冒泡排序、堆排序、快速排序。
Out-place sort:归并排序、计数排序、基数排序、桶排序。

当需要对大量数据进行排序时,In-place sort就显示出优点,因为只需要占用常数的内存。
设想一下,如果要对10000个数据排序,如果使用了Out-place sort,则假设需要用200G的额外空间,则一台老式电脑会吃不消,但是如果使用In-place sort,则不需要花费额外内存。

stable sort:插入排序、冒泡排序、归并排序、计数排序、基数排序、桶排序。
unstable sort:选择排序(5 8 5 2 9)、快速排序、堆排序。

为何排序的稳定性很重要?

在初学排序时会觉得稳定性有这么重要吗?两个一样的元素的顺序有这么重要吗?其实很重要。在基数排序中显得尤为突出,如下:




算法导论习题8.3-2说:如果对于不稳定的算法进行改进,使得那些不稳定的算法也稳定?
其实很简单,只需要在每个输入元素加一个index,表示初始时的数组索引,当不稳定的算法排好序后,对于相同的元素对index排序即可。

基于比较的排序都是遵循“决策树模型”,而在决策树模型中,我们能证明给予比较的排序算法最坏情况下的运行时间为Ω(nlgn),证明的思路是因为将n个序列构成的决策树的叶子节点个数至少有n!,因此高度至少为nlgn。

线性时间排序虽然能够理想情况下能在线性时间排序,但是每个排序都需要对输入数组做一些假设,比如计数排序需要输入数组数字范围为[0,k]等。

在排序算法的正确性证明中介绍了”循环不变式“,他类似于数学归纳法,"初始"对应"n=1","保持"对应"假设n=k成立,当n=k+1时"。

一、插入排序


特点:stable sort、In-place sort
最优复杂度:当输入数组就是排好序的时候,复杂度为O(n),而快速排序在这种情况下会产生O(n^2)的复杂度。
最差复杂度:当输入数组为倒序时,复杂度为O(n^2)
插入排序比较适合用于“少量元素的数组”。

其实插入排序的复杂度和逆序对的个数一样,当数组倒序时,逆序对的个数为n(n-1)/2,因此插入排序复杂度为O(n^2)。
在算法导论2-4中有关于逆序对的介绍。

伪代码:


证明算法正确性:

循环不变式:在每次循环开始前,A[1...i-1]包含了原来的A[1...i-1]的元素,并且已排序。

初始:i=2,A[1...1]已排序,成立。
保持:在迭代开始前,A[1...i-1]已排序,而循环体的目的是将A[i]插入A[1...i-1]中,使得A[1...i]排序,因此在下一轮迭代开       始前,i++,因此现在A[1...i-1]排好序了,因此保持循环不变式。
终止:最后i=n+1,并且A[1...n]已排序,而A[1...n]就是整个数组,因此证毕。

而在算法导论2.3-6中还问是否能将伪代码第6-8行用二分法实现?

实际上是不能的。因为第6-8行并不是单纯的线性查找,而是还要移出一个空位让A[i]插入,因此就算二分查找用O(lgn)查到了插入的位置,但是还是要用O(n)的时间移出一个空位。

问:快速排序(不使用随机化)是否一定比插入排序快?

答:不一定,当输入数组已经排好序时,插入排序需要O(n)时间,而快速排序需要O(n^2)时间。

递归版插入排序




二、冒泡排序


特点:stable sort、In-place sort
思想:通过两两交换,像水中的泡泡一样,小的先冒出来,大的后冒出来。
最坏运行时间:O(n^2)
最佳运行时间:O(n^2)(当然,也可以进行改进使得最佳运行时间为O(n))

算法导论思考题2-2中介绍了冒泡排序。

伪代码:



证明算法正确性:

运用两次循环不变式,先证明第4-6行的内循环,再证明外循环。

内循环不变式:在每次循环开始前,A[j]是A[j...n]中最小的元素。

初始:j=n,因此A[n]是A[n...n]的最小元素。
保持:当循环开始时,已知A[j]是A[j...n]的最小元素,将A[j]与A[j-1]比较,并将较小者放在j-1位置,因此能够说明A[j-1]是A[j-1...n]的最小元素,因此循环不变式保持。
终止:j=i,已知A[i]是A[i...n]中最小的元素,证毕。

接下来证明外循环不变式:在每次循环之前,A[1...i-1]包含了A中最小的i-1个元素,且已排序:A[1]<=A[2]<=...<=A[i-1]。

初始:i=1,因此A[1..0]=空,因此成立。
保持:当循环开始时,已知A[1...i-1]是A中最小的i-1个元素,且A[1]<=A[2]<=...<=A[i-1],根据内循环不变式,终止时A[i]是A[i...n]中最小的元素,因此A[1...i]包含了A中最小的i个元素,且A[1]<=A[2]<=...<=A[i-1]<=A[i]
终止:i=n+1,已知A[1...n]是A中最小的n个元素,且A[1]<=A[2]<=...<=A[n],得证。

在算法导论思考题2-2中又问了”冒泡排序和插入排序哪个更快“呢?

一般的人回答:“差不多吧,因为渐近时间都是O(n^2)”。
但是事实上不是这样的,插入排序的速度直接是逆序对的个数,而冒泡排序中执行“交换“的次数是逆序对的个数,因此冒泡排序执行的时间至少是逆序对的个数,因此插入排序的执行时间至少比冒泡排序快。


递归版冒泡排序




改进版冒泡排序


最佳运行时间:O(n)
最坏运行时间:O(n^2)



三、选择排序


特性:In-place sort,unstable sort。
思想:每次找一个最小值。
最好情况时间:O(n^2)。
最坏情况时间:O(n^2)。

伪代码:


证明算法正确性:

循环不变式:A[1...i-1]包含了A中最小的i-1个元素,且已排序。

初始:i=1,A[1...0]=空,因此成立。
保持:在某次迭代开始之前,保持循环不变式,即A[1...i-1]包含了A中最小的i-1个元素,且已排序,则进入循环体后,程序从         A[i...n]中找出最小值放在A[i]处,因此A[1...i]包含了A中最小的i个元素,且已排序,而i++,因此下一次循环之前,保持       循环不变式:A[1..i-1]包含了A中最小的i-1个元素,且已排序。
终止:i=n,已知A[1...n-1]包含了A中最小的i-1个元素,且已排序,因此A[n]中的元素是最大的,因此A[1...n]已排序,证毕。


算法导论2.2-2中问了"为什么伪代码中第3行只有循环n-1次而不是n次"?

在循环不变式证明中也提到了,如果A[1...n-1]已排序,且包含了A中最小的n-1个元素,则A[n]肯定是最大的,因此肯定是已排序的。


递归版选择排序



递归式:

T(n)=T(n-1)+O(n) 
=> T(n)=O(n^2)

四、归并排序


特点:stable sort、Out-place sort
思想:运用分治法思想解决排序问题。
最坏情况运行时间:O(nlgn)
最佳运行时间:O(nlgn)

分治法介绍:分治法就是将原问题分解为多个独立的子问题,且这些子问题的形式和原问题相似,只是规模上减少了,求解完子问题后合并结果构成原问题的解。
分治法通常有3步:Divide(分解子问题的步骤) 、 Conquer(递归解决子问题的步骤)、 Combine(子问题解求出来后合并成原问题解的步骤)。
假设Divide需要f(n)时间,Conquer分解为b个子问题,且子问题大小为a,Combine需要g(n)时间,则递归式为:
T(n)=bT(n/a)+f(n)+g(n)

算法导论思考题4-3(参数传递)能够很好的考察对于分治法的理解。

就如归并排序,Divide的步骤为m=(p+q)/2,因此为O(1),Combine步骤为merge()函数,Conquer步骤为分解为2个子问题,子问题大小为n/2,因此:
归并排序的递归式:T(n)=2T(n/2)+O(n)

而求解递归式的三种方法有:
(1)替换法:主要用于验证递归式的复杂度。
(2)递归树:能够大致估算递归式的复杂度,估算完后可以用替换法验证。
(3)主定理:用于解一些常见的递归式。

伪代码:



证明算法正确性:

其实我们只要证明merge()函数的正确性即可。
merge函数的主要步骤在第25~31行,可以看出是由一个循环构成。

循环不变式:每次循环之前,A[p...k-1]已排序,且L[i]和R[j]是L和R中剩下的元素中最小的两个元素。
初始:k=p,A[p...p-1]为空,因此已排序,成立。
保持:在第k次迭代之前,A[p...k-1]已经排序,而因为L[i]和R[j]是L和R中剩下的元素中最小的两个元素,因此只需要将L[i]和R[j]中最小的元素放到A[k]即可,在第k+1次迭代之前A[p...k]已排序,且L[i]和R[j]为剩下的最小的两个元素。
终止:k=q+1,且A[p...q]已排序,这就是我们想要的,因此证毕。

归并排序的例子:


问:归并排序的缺点是什么?

答:他是Out-place sort,因此相比快排,需要很多额外的空间。

问:为什么归并排序比快速排序慢?

答:虽然渐近复杂度一样,但是归并排序的系数比快排大。

问:对于归并排序有什么改进?

答:就是在数组长度为k时,用插入排序,因为插入排序适合对小数组排序。在算法导论思考题2-1中介绍了。复杂度为O(nk+nlg(n/k)) ,当k=O(lgn)时,复杂度为O(nlgn)

五、快速排序


Tony Hoare爵士在1962年发明,被誉为“20世纪十大经典算法之一”。
算法导论中讲解的快速排序的PARTITION是Lomuto提出的,是对Hoare的算法进行一些改变的,而算法导论7-1介绍了Hoare的快排。
特性:unstable sort、In-place sort。
最坏运行时间:当输入数组已排序时,时间为O(n^2),当然可以通过随机化来改进(shuffle array 或者 randomized select pivot),使得期望运行时间为O(nlgn)。
最佳运行时间:O(nlgn)
快速排序的思想也是分治法。
当输入数组的所有元素都一样时,不管是快速排序还是随机化快速排序的复杂度都为O(n^2),而在算法导论第三版的思考题7-2中通过改变Partition函数,从而改进复杂度为O(n)。

注意:只要partition的划分比例是常数的,则快排的效率就是O(nlgn),比如当partition的划分比例为10000:1时(足够不平衡了),快排的效率还是O(nlgn)

“A killer adversary for quicksort”这篇文章很有趣的介绍了怎么样设计一个输入数组,使得quicksort运行时间为O(n^2)。

伪代码:



随机化partition的实现:



改进当所有元素相同时的效率的Partition实现:



证明算法正确性:

对partition函数证明循环不变式:A[p...i]的所有元素小于等于pivot,A[i+1...j-1]的所有元素大于pivot。
初始:i=p-1,j=p,因此A[p...p-1]=空,A[p...p-1]=空,因此成立。
保持:当循环开始前,已知A[p...i]的所有元素小于等于pivot,A[i+1...j-1]的所有元素大于pivot,在循环体中,
            - 如果A[j]>pivot,那么不动,j++,此时A[p...i]的所有元素小于等于pivot,A[i+1...j-1]的所有元素大于pivot。
            - 如果A[j]<=pivot,则i++,A[i+1]>pivot,将A[i+1]和A[j]交换后,A[P...i]保持所有元素小于等于pivot,而A[i+1...j-1]的所有元素大于pivot。
终止:j=r,因此A[p...i]的所有元素小于等于pivot,A[i+1...r-1]的所有元素大于pivot。

六、堆排序


1964年Williams提出。

特性:unstable sort、In-place sort。
最优时间:O(nlgn)
最差时间:O(nlgn)
此篇文章介绍了堆排序的最优时间和最差时间的证明:http://blog.csdn.net/xiazdong/article/details/8193625 
思想:运用了最小堆、最大堆这个数据结构,而堆还能用于构建优先队列。

优先队列应用于进程间调度、任务调度等。
堆数据结构应用于Dijkstra、Prim算法。



证明算法正确性:

(1)证明build_max_heap的正确性:
循环不变式:每次循环开始前,A[i+1]、A[i+2]、...、A[n]分别为最大堆的根。

初始:i=floor(n/2),则A[i+1]、...、A[n]都是叶子,因此成立。
保持:每次迭代开始前,已知A[i+1]、A[i+2]、...、A[n]分别为最大堆的根,在循环体中,因为A[i]的孩子的子树都是最大堆,因此执行完MAX_HEAPIFY(A,i)后,A[i]也是最大堆的根,因此保持循环不变式。
终止:i=0,已知A[1]、...、A[n]都是最大堆的根,得到了A[1]是最大堆的根,因此证毕。

(2)证明heapsort的正确性:
循环不变式:每次迭代前,A[i+1]、...、A[n]包含了A中最大的n-i个元素,且A[i+1]<=A[i+2]<=...<=A[n],且A[1]是堆中最大的。

初始:i=n,A[n+1]...A[n]为空,成立。
保持:每次迭代开始前,A[i+1]、...、A[n]包含了A中最大的n-i个元素,且A[i+1]<=A[i+2]<=...<=A[n],循环体内将A[1]与A[i]交换,因为A[1]是堆中最大的,因此A[i]、...、A[n]包含了A中最大的n-i+1个元素且A[i]<=A[i+1]<=A[i+2]<=...<=A[n],因此保持循环不变式。
终止:i=1,已知A[2]、...、A[n]包含了A中最大的n-1个元素,且A[2]<=A[3]<=...<=A[n],因此A[1]<=A[2]<=A[3]<=...<=A[n],证毕。

七、计数排序


特性:stable sort、out-place sort。
最坏情况运行时间:O(n+k)
最好情况运行时间:O(n+k)

当k=O(n)时,计数排序时间为O(n)

伪代码:



八、基数排序


本文假定每位的排序是计数排序。
特性:stable sort、Out-place sort。
最坏情况运行时间:O((n+k)d)
最好情况运行时间:O((n+k)d)

当d为常数、k=O(n)时,效率为O(n)
我们也不一定要一位一位排序,我们可以多位多位排序,比如一共10位,我们可以先对低5位排序,再对高5位排序。
引理:假设n个b位数,将b位数分为多个单元,且每个单元为r位,那么基数排序的效率为O[(b/r)(n+2^r)]。
当b=O(nlgn),r=lgn时,基数排序效率O(n)

比如算法导论习题8.3-4:说明如何在O(n)时间内,对0~n^2-1之间的n个整数排序?
答案:将这些数化为2进制,位数为lg(n^2)=2lgn=O(lgn),因此利用引理,b=O(lgn),而我们设r=lgn,则基数排序可以在O(n)内排序。

基数排序的例子:




证明算法正确性:

通过循环不变式可证,证明略。

九、桶排序


假设输入数组的元素都在[0,1)之间。
特性: out-place sort、stable sort
最坏情况运行时间:当分布不均匀时,全部元素都分到一个桶中,则O(n^2),当然[算法导论8.4-2]也可以将插入排序换成堆排序、快速排序等,这样最坏情况就是O(nlgn)。
最好情况运行时间:O(n)

桶排序的例子:


伪代码:



证明算法正确性:

对于任意A[i]<=A[j],且A[i]落在B[a],A[j]落在B[b],我们可以看出a<=b,因此得证。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值