前言
编程珠玑一书对快速排序讲得较为透彻,最早的快排是单向的,慢慢演化成双向的,也就是目前的版本。从此书能看到这种演化的必要性。我想这是我最后一次看快排,原理搞懂了就不会忘了。
快速排序思想
1962年,由C.A.R.Hoare创造出来。该算法核心思想就一句话:“排序数组时,将数组分成两个小部分,然后对它们递归排序”。然而采取什么样的策略将数组分成两个部分是关键,想想看,如果随便将数组A分成A1和A2两个小部分,即便分别将A1和A2排好序,那么A1和A2重新组合成A时仍然是无序的。所以,我们可以在数组中找一个值,称为key值,我们在把数组A分解为A1和A2前先对A做一些处理,让小于key值的元素都移到其左边,所有大于key值的元素都移到其右边。这样递归排序A1和A2,数组A就排好了。
举例
我们要排序的数组如下:
55 | 41 | 59 | 26 | 53 | 58 | 97 | 93 |
我们选取第一个元素作为key值,即55.(一般都是选取第一个元素)。假如我们有一种办法可以将数组做一步预处理,让小于key值的元素都位于其左边,大于key值的元素都位于其右边,预处理完数组如下:
41 | 26 | 53 | 55 | 59 | 58 | 97 | 93 |
/*l, u 代表待排序部分的下界和上界*/ void qsort(l, u) { /*递归结束条件是待排序部分的元素个数小于2*/ if(l >= u) { return; } /*此处进行预处理,预处理后key值位于位置p*/ qsort(l, p-1); qsort(p+1, u); }
接下来看如何做预处理。我们选取A[0]做为key值, p作为key值的位置。我们从A[1]开始遍历后面的数组,用变量i指示目前的位置,每次找到小于key的值都与A[++p]交换。开始时p=0.
55 | 41 | 59 | 26 | 53 | 58 | 97 | 93 |
55 | 41 | 59 | 26 | 53 | 58 | 97 | 93 |
55 | 41 | 26 | 59 | 53 | 58 | 97 | 93 |
55 | 41 | 26 | 53 | 59 | 58 | 97 | 93 |
最后swap(l, p)即把Key值放到最终位置上了。至于为什么要交换l,p的位置,可以另拿一组数据试一下:55,41,59,26,99,58,97,93。
完整的程序1
/*l, u 代表待排序部分的下界和上界*/ void qsort(int l, int u) { /*递归结束条件是待排序部分的元素个数小于2*/ if(l >= u) { return; } int p = l; for(int i = l+1; i <= u; i++) { if(A[i] < A[l]) { swap(++p, i); } } swap(l, p); qsort(l, p-1); qsort(p+1, u); }
这就是第一代快速排序算法,正常情况下其复杂度为nlogn,但在考虑一种极端情况:n个相同元素组成的数组。在n-1次划分中每次划分都需要O(n)的时间,所以总的时间为O(n^2)。使用双向划分就可以避免这个问题。
双向划分快速排序
/*l, u 代表待排序部分的下界和上界*/ void qsort(int l, int u) { /*递归结束条件是待排序部分的元素个数小于2*/ if(l >= u) { return; } key = A[l] for(int i = l, j = u+1; i <= j;) { do i++ while(i <= u && A[i] < key)); do j-- while(A[j] > key); if(i > j) { break; } swap(i, j); } swap(l, j); qsort(l, j-1); qsort(j+1, u); }