如果你想学习、提高改进QuickSort的实现,建议先看 Engineering a sort function; Jon Bentley and M. Douglas McIlroy; Software - Practice and Experience; Vol. 23 (11), 1249-1265, 1993.
算法的基本思想:
快速排序的基本思想是基于分治策略的。对于输入的子序列L[p..r],如果规模足够小则直接进行排序,否则分三步处理:
* 分解(Divide):将输入的序列L[p..r]划分成两个非空子序列L[p..q]和L[q+1..r],使L[p..q]中任一元素的值不大于L[q+1..r]中任一元素的值。
* 递归求解(Conquer):通过递归调用快速排序算法分别对L[p..q]和L[q+1..r]进行排序。
* 合并(Merge):由于对分解出的两个子序列的排序是就地进行的,所以在L[p..q]和L[q+1..r]都排好序后不需要执行任何计算 L[p..r]就已排好序。
这个解决流程是符合分治法的基本步骤的。因此,快速排序法是分治法的经典应用实例之一。
C代码
void quick_sort(int *a, int n)
{
int m;
if (n <= 1) return;
m = patition(a, n);
quick_sort (a, m);
quick_sort (a + m + 1, n - m - 1);
}
选择Partition元素:
1.很多实现用第一个或最后一个作为pivot,这种在大多数情况还是有很好的性能,但是对于一些特殊输入如:大部分已有偏序的序列,将是性能最坏的时候。
2.随机选择一个数做pivot,相比已方法1,可以干掉特殊系列,不过随机导入的大多数情况只能有平均性能。
3.尽量选择偏序后的中间元素,使得算法逼近 nlogn: 这里有Tukey’s ‘ninther’, the median of the medians of three samples、 each of three elements.
Partition的方法:要考虑如何处理与pivot相等的元素?和性能有很大关系。
1. lemuto's Partition
My Code:
int patition_lomuto(int *a, int n)
{
int i, m;
swap (a + rand () % n, a + n - 1);
for (i = 0, m = 0; i < n - 1; i++)
if (a[i] <= a[n - 1]) {
swap (a + i, a + m);
m = m + 1;
}
swap (a + m, a + n - 1);
return m;
}
这里我把pivot放在最后一个元素,不过是一样的。还有这里的采用上述提到的方法2 来选择pivot.
2. 用两个指针大部分算法提到的,相比于方法1,比较的次数下降,效率有明显的提高
int patition_twoptr(int *a, int n)
{
int i, j;
swap(a, a + rand() % n);
i = 0; j = n;
for (;;){
do i++; while (i < n && a[i] < a[0]);
do j--; while (a[j] > a[0]);
if (i > j) break;
swap(a+i, a+j);
}
swap(a, a+j);
return j;
}
3. 对于两个指针优化,妥善处理了与pivot相等的元素的问题,参考经典缺陷2
经典的有:Dijkstra’s ‘Dutch National Flag’
但是其编码复杂,而且要达到好效率似乎要花一翻功夫。
void quick_sort2(int *x, int n)
{
int a, b, c, d, l, h, s, v;
if (n <= 1) return;
v = x[rand () % n];
a = b = 0;
c = d = n - 1;
for (;;) {
while (b <= c && x[b] <= v) {
if (x[b] == v) swap (x + a++, x + b);
b++;
}
while (c >= b && x[c] >= v) {
if (x[c] == v) swap (x + c, x + d--);
c--;
}
if (b > c) break;
swap (x + b++, x + c--);
}
s = min (a, b - a);
for (l = 0, h = b - s; s; s--)
swap (x + l++, x + h++);
s = min (n - 1 - d, d - c);
for (l = b, h = n - s; s; s--)
swap (x + l++, x + h++);
quick_sort2 (x, b - a);
quick_sort2 (x + n - (d - c), d - c);
}
著名的性能缺陷:
1. Scowen’s ‘Quickersort’ in Version Seventh of Unix System, took n2 comparisons to sort an ‘organpipe’ array of 2n integers: 123..nn.. 321.
2. Quickersort in Berkeley 1983 consume quadratic time on arrays that contain a few elements repeated many times—in particular arrays of random zeros and ones.
至于Tukey’s ‘ninther’ method, 以及更多的性能优化,请google.
关于qsort的库实现的亮点,简单的提及下,后面我也粘贴了个参考贴。
1.库中用了一个很精练的Stack避免了递归调用
2.用了median-of-three decision tree来选择pivot元素
3.在元素个数小于某MAX_THRESH 时用insertsort,大于MAX_THRESH时用quickSort.
PS: 1,2 两点的改进处于大师Ken Thompson’s之手, 有人不认识Ken? C 和Unix的创始人
参考材料:
And Here is a video by Jon Bentley: http://www.youtube.com/watch?v=aMnn0Jq0J-E
http://en.wikipedia.org/wiki/Quick_sort
P.S.:这里有搜索到站内的对于qsort库实现的分析:
http://blog.youkuaiyun.com/ghame/archive/2006/05/18/744547.aspx
http://blog.youkuaiyun.com/ghame/archive/2006/05/20/746150.aspx
http://blog.youkuaiyun.com/ghame/archive/2006/05/21/747911.aspx