排序算法之快速排序

参考过以下博客,在此表示感谢:
1. 白话经典算法系列之六 快速排序 快速搞定
2. 坐在马桶上看算法:快速排序

1. 基本思路(挖坑填数 + 分而治之)

  • 1.1 从数组A中取出一个数作为基准数,比如说取A[0],将A[0]保存到x中,这时可以看作已经在元素A[0]处挖了一个,可以将其他数据填充到这里来。初始化,i = 0(left),j = 9(right),x = A[0] = 9
0 1 2 3 4 5 6 7 8 9
9 4 6 3 20 1 8 15 17 11
  • 1.2 先从j开始从后向前开始查找,遇到小于x的数,就将其填充到已挖好的坑A[0]处。此处,j = 6时,A[j] = 8,填充到坑A[0]处,此时相当于在A[j] = A[6]处又挖了一个新坑,下个数据可以填充到这里。又因为A[0]的坑已经填充了数据,故左下标值i要加一。这样,i = 1,j = 6。
0 1 2 3 4 5 6 7 8 9
8 4 6 3 20 1 8 15 17 11
  • 1.3 接着,从i = 1开始,从前向后查找,当遇到大于等于x的数,就将其填充到坑A[6]处。当i = 4,此时A[i] = A[4] = 20 > x,故将其填充到坑A[j] = A[6]处,j减一,并在A[i] = A[4]处形成新坑。这样,i = 4,j = 5。
0 1 2 3 4 5 6 7 8 9
8 4 6 3 20 1 20 15 17 11
  • 1.4 再从j开始,向前查找,当j = 5时,A[j] = 1 < x,此时将A[5]填入坑A[4]中,i加一,而A[5]成为新坑。这时,i = 5,j = 5。
0 1 2 3 4 5 6 7 8 9
8 4 6 3 1 1 20 15 17 11
  • 1.5 接着,从i = 5开始向右开始查找,由于i = j = 5,查找结束。这时,将x填入坑A[5]中,即另A[5] = 9,第一次排序完成。这时,A[5]前面的数都小于它,A[5]后面的数都大于它。
0 1 2 3 4 5 6 7 8 9
8 4 6 3 1 9 20 15 17 11
  • 1.6 再对两个分支,A[0…4]和A[6…9]重复上述步骤就可以完成排序。

2. 步骤总结

  1. i = L, j = R,将基准数挖出形成第一个坑A[i]
  2. j–由后向前找出比基准数小的数,找出后挖出此数填入前一个坑A[i]中,对应的i加一
  3. i++由前向后找出大于等于基准数的数,找到后挖出此数填到前一个坑A[j]中,对应的j加一
  4. 重复执行2,3步,直到i == j,将基准数填入A[i]中

参照此步骤,很容易写出挖坑填数的代码:

 1 int i = L, j = R, x = A[L];     //A[L]即A[i]就是第一个坑
 2 while(i < j)
 3 {
 4     //从后向前找小于x的数来填坑A[i]
 5     while(i < j)
 6     {
 7         if(A[j] < x)
 8         {
 9             A[i] = A[j];    //将A[j]填入A[i]中,这样A[j]就成了新坑
10             i++;            //原来的坑A[i]已经填好,i值加一
11             break;
12         }
13         else
14             j--;
15     }
16     //从前向后找大于或等于x的数来填A[j]
17     while(i < j)
18     {
19         if(A[i] >= x)
20         {
21             A[j] = A[i];    //将A[i]填入A[j]中,这样A[i]就成了新坑
22             j--;            //原来的坑A[j]已经填好,j值减一
23             break;
24         }
25         else
26             i++;
27     }
28 }
29 //退出时,i = j。将x填入这个坑中
30 A[i] = x;

 

进一步改写可得:

 1 int i = L, j = R, x = A[L];
 2 while(i < j)
 3 {
 4     while(A[j] >= x && i < j)       //从后往前找小于x的数
 5         j--;
 6     if(i < j)
 7         A[i++] = A[j];
 8     while(A[i] < x && i < j)        //从前往后找大于或等于x的数
 9         i++;
10     if(i < j)
11         A[i++] = A[j];
12 }
13 //退出时,i = j。将x填入这个坑中
14 A[i] = x;

 

3. 完整代码实现

完整的代码实现如下:

 1 //override
 2 void quickSortOverride(int A[], int n)
 3 {
 4     //利用重载,简化程序的入口
 5     quickSortOverride(A, 0, n-1);
 6 }
 7 
 8 void quickSortOverride(int A[], int lo, int hi)
 9 {
10     if (lo < hi)        //如果lo = hi,即只有一个元素时,已经是有序的了,不需要处理
11     {
12         int i = lo, j = hi, x = A[lo];
13         while (i < j)
14         {
15             while (i < j && A[j] >= x)
16                 j--;
17             if (i < j)
18                 A[i++] = A[j];
19 
20             while (i < j && A[i] < x)
21                 i++;
22             if (i < j)
23                 A[j--] = A[i];
24         }
25         //退出时,i = j。将x填入这个坑中
26         A[i] = x;
27         quickSortOverride(A, lo, i-1);      //左边继续递归
28         quickSortOverride(A, j+1, hi);      //右边继续递归
29     }
30 }

 

4. 正确性分析

  1. 不变形
    经过k次排序,整个数组里已有k个元素有序,对于这k个元素中的任一元素,左边的所有元素都比它小,右边的所有元素都比它大。
  2. 单调性
    经过k次排序,相对无序的元素个数缩减至n-k
  3. 正确性
    经过至多n次排序后,算法必然终止,数组的元素都是有序的

4.1 当选择最左边的元素A[lo]作为基准值时,为什么必须第一次从后往前扫描?

假如是从前往后扫描,当扫描到第一个大于或等于x的元素A[i],必须将这个元素填入先前的坑A[lo]中,这样就已经出现了错误,导致最终i = j时,A[lo…i-1]的元素不是都小于A[i]的,至少A[lo]就已经不满足了。

5. 复杂度分析

为求解qSort(A, lo, hi)规模为n的问题,需要递归求解两个规模为n/2的问题qSort(A, lo, (lo+hi)/2-1)和qSort(A, (lo+hi)/2+1, hi),以及最坏情况下至多n次的比较填充操作。
而递归基为:qSort(A, lo ,hi),其中lo = hi,所需的时间为O(1)。
故可得到如下递推方程:
T(n) = 2*T(n/2) + n;
T(1) = O(1);
求解:
T(n) = 2*T(n/2) + n;
T(n/2) = 2*T(n/22) + n/2;
T(n/22) = 2*T(n/23) + n/22;
……
T(2) = 2*T(1) + 2 = 2*T(n/2log2n) + n/2log2(n-1);
T(1) = O(1);
继而可得:
T(n) = 2*T(n/2) + n;
2*T(n/2) = 22*T(n/22) + n;
22*T(n/22) = 23*T(n/23) + n;
……
2log2(n-1)T(2) = 2log2n*T(1) + n;
2log2nT(1) = 2log2nO(1) = n;
可得:
T(n) = n*(log2n - 0 + 1) = n*(log2n + 1) = O(nlogn)
即最坏情况下的时间复杂度为:
T(n) = O(nlogn)

6. 另一种代码实现思路

仍然是先选取一基准值,初始情况,可另i = lo, j = hi, x = A[lo]
也是先从后向前查找小于x的数,当找到时,记录下此时的j值。
接着从前向后查找大于x的数(注意此处是大于,不是上面一种算法实现的大于或等于),当找到时,记录下此时的i值。
然后,交换对应的A[i]和A[j],如果i仍然小于j,重复上述步骤,直至i = j。紧接着,交换A[lo]和A[i]的值,至此做完了一次排序。这是A[lo…i-1]的各元素都小于等于A[i],A[i+1…hi]的各元素都大于等于A[i]。

1. 代码实现

 1 //override
 2 void quickSortSwap(int A[], int n)
 3 {
 4     //利用重载,简化程序的接口
 5     quickSortSwap(A, 0, n-1);
 6 }
 7 
 8 void mySwap(int &x1, int &x2)
 9 {
10     int tmp = x1;
11     x1 = x2;
12     x2 = tmp;
13 }
14 
15 void quickSortSwap(int A[], int lo, int hi)
16 {
17     if (lo < hi)
18     {
19         int i = lo, j = hi, x = A[lo];
20         while (i < j)
21         {
22             while (i < j && A[j] >= x)
23                 j--;
24 
25             while (i < j && A[i] <= x)      //注意此处必须是小于等于
26                 i++;
27             if (i < j)
28                 mySwap(A[i], A[j]);     //注意此处不能让i或j自加或者自减
29         }
30         mySwap(A[lo], A[i]);
31         quickSortSwap(A, lo, i-1);
32         quickSortSwap(A, j+1, hi);
33     }
34 }

 

2. 正确性分析

2.1 当选择最左边的元素A[lo]作为基准值时,为什么必须第一次从后往前扫描?

若第一次从后往前扫描。交换总是成对进行的。进行完一次交换后,必然再一次从后往前扫描,分两种情况:1. 若j一直自减,没有发现比x小的元素,直至i = j终止,此刻停在之前完成交换的元素A[i]处,由于已经是参与过交换的元素,故A[i]一定小于x,这时,要拿x与A[i]交换,不会影响整体的有序性。2. 若j一直自减,中途发现了比x小的元素,记录下此时j的值。然后从前往后扫描,没有发现大于x的值,直至i = j,此时停在之前保留的j值的地方。由于还未完成一次交换,A[j]小于x,这时,要拿x和A[j]进行交换,也不会影响整体的有序性。而要做到这些,程序里必须保证在进行一次交换后,不要让i或j的值自增或者自减,而是让程序再次扫描时处理。
反之,如果第一次从前往后扫描,同样可以按照这个方法分析,就不能保证整体的有序性。

2.2 为什么从前往后扫描的判断条件不能是小于x而必须是小于或等于x?

如果是小于x,那么第一次从前往后扫描时,就会将左边缘的基准值A[lo]记录下来,参与交换,这样如果一次排序结束,是直接交换先前保留的x的值和A[i]的值,那么会出现A[i]的值被覆盖为x的值,而之前x的值已经参与了交换,这样x的值就会重复出现。如果一次排序结束,是交换A[lo]和A[i]的值,那么会出现最终停在的i = j的位置上并不是之前选定的基准值x,而是其他元素。这样就出现了A[lo…i-1]各元素的值都小于那个其他元素,A[i+1…hi]各元素的值都大于那个其他元素,而不是基准值。

 

posted @ 2015-10-25 16:30  志科  阅读(417)  评论(0编辑  收藏  举报