排序算法之快速排序
参考过以下博客,在此表示感谢:
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. 步骤总结
- i = L, j = R,将基准数挖出形成第一个坑A[i]
- j–由后向前找出比基准数小的数,找出后挖出此数填入前一个坑A[i]中,对应的i加一
- i++由前向后找出大于等于基准数的数,找到后挖出此数填到前一个坑A[j]中,对应的j加一
- 重复执行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. 正确性分析
- 不变形
经过k次排序,整个数组里已有k个元素有序,对于这k个元素中的任一元素,左边的所有元素都比它小,右边的所有元素都比它大。 - 单调性
经过k次排序,相对无序的元素个数缩减至n-k - 正确性
经过至多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]各元素的值都大于那个其他元素,而不是基准值。