背景
快速排序 (Quicksort) 由 C.A.R. Hoare (著名计算机科学家,1980年图灵奖得主) 于1962年提出。他在 Computer Journal 中发表的经典论文 “Quicksort” 中描述了这一算法。采用了分治法的策略。
基本思想
- 先从数列中取出一个数作为基准数
- 将比这个数小的放到它的左边,比这个数大的放到它的右边
- 对左右区间重复上一步,直到各个区间只有一个数
算法复杂度
- 最坏情况复杂度为
O(n2)
每个内点只有一个叶子结点。 比较次数为:
0+1+2+...+n=n∗(n+1)2
- 最好情况复杂度为
O(nlog2n)
每个内点有两个叶子结点,若叶子数为n,内点数为 n - 1 ,顶点数为 2n - 1, 叶子到根的距离为
O(log2n)(上下取整)。顶点到根的距离之和 = 2 * 叶子到跟的距离之和 - 顶点数 + 1
比较次数约为
2∗nlog2n−(n−1)≈nlog2n−2n+1
- 平均时间复杂度为
O(nlog2n)
平均情况下,Tn为n个对象平均比较次数,若取第k个数为分割标准,则一个序列有 k - 1 个数,另一个序列有 n - k 个数。
⎧⎩⎨⎪⎪Tn=1n∑k=1n(n−1+Tk−1+Tn−k)=n−1+2n∑k=0n−1TkT2=1,T1=0,T0=0求解:
⎧⎩⎨⎪⎪⎪⎪nTn=n(n−1)+2∑k=0n−1Tk(n+1)Tn+1=(n+1)n+2∑k=0nTk相减:
(n+1)Tn+1=(n+2)Tn+2n
Tn+1n+2=Tnn+1+2n(n+1)(n+2)变换:
Sn=Tnn+1
⎧⎩⎨⎪⎪Sn+1−Sn=2n(n+1)(n+2)S0=0,S1=0,S2=13
Sn=∑k=0n−12k(k+1)(k+2)=4∑k=0n−11k+2−2∑k=0n−11k+1=2∑k=2n1k+4n+1−2估计和,用求和进行逼近,只需上界
∑k=2n1k<∫n11xdx=ln(n)−ln(1)=ln(n)
Sn<2ln(n)+2n+1+2=2ln(n)+O(1)调和极数求和:
1+12+13+...+1n=ln(n+1)+r
这里r是欧拉常数约为0.57721。则:
Tn=2(n+1)ln(n)+O(n)=1.3863nlog2n+O(n)
算法实现
quicksort 1
通过从左到右的扫描完成排序。容易忽略swap(l,m)这一步 (移动哨兵),而当t是子数组中严格最大的元素时,会导致死循环。
在一些常见输入下,可能退化为平方时间算法。
void quick_sort(int l, int r)
{
if (l < r)
{
int i = l, j = r, m = l;
for( i = l + 1; i < r; i++)
{
if(x[i] < x[l])
swap(++m,i);
}
swap(l, m);
quick_sort(l, m - 1); // 递归调用
quick_sort(m + 1, r);
}
}
quicksort 2
将划分方案改为从右向左进行。循环终止时,x[m] == x[l],直接使用参(l,m-1)和(m+1,r)递归,不再需要swap操作。
void quick_sort(int l, int r)
{
if (l < r)
{
int i,m = r + 1;
for( i = r ; i >= l; i--)
{
if(x[i] >= x[l])
swap(--m,i);
}
quick_sort(l, m - 1); // 递归调用
quick_sort(m + 1, r);
}
}
考虑极端情况,如n个相同元素组成的数组,对于这种输入,插入排序的性能非常好,每个元素移动距离都为0。 而quicksort 1的性能却非常糟糕,n - 1 次划分每次都要 O(n) 的时间来去掉一个元素。 总时间是
O(n2)。采用双向划分可以避免这个问题。
下标i 和 j 初始化为数组的两端,主循环中有两个内循环,第一个循环向右移动遇到较大元素时停止,第二个循环向左移动遇到较小元素时停止。然后主循环测试两个下标是否交叉并替换值。当元素相同时,停止扫描并交换i和j的值,这样虽然交换次数增加了,但是将所有元素相同的最坏情况变成了差不多O(nlog2n)的最好情况。如下:
quicksort 3
void quick_sort(int l, int r)
{
if (l < r)
{
int i = l, j = u + 1, t = x[l];
while(1)
{
while(i <= r && x[i] < t) i++;
while(x[j] > t) j--;
if(i > j)
break;
swap(i,j);
}
swap(i,j);
quick_sort(l, j - 1); // 递归调用
quick_sort(j + 1, r);
}
}
quicksort 4
下面是一种常见的双向写法。
void quick_sort(int s[], int l, int r)
{
if (l < r)
{
int i = l, j = r, x = s[l];
while (i < j)
{
while(i < j && s[j] >= x) // 从右向左找第一个小于x的数
j--;
if(i < j)
s[i++] = s[j];
while(i < j && s[i] < x) // 从左向右找第一个大于等于x的数
i++;
if(i < j)
s[j--] = s[i];
}
s[i] = x;
quick_sort(s, l, i - 1); // 递归调用
quick_sort(s, i + 1, r);
}
}
算法优化
- 上面的快速排序都是围绕第一个元素进行的。对于随机的输入,这样没有问题,但是对于某些常见的输入,这样做需要的时间和空间更多, 如数组已经升序时。随机选择哨兵可以得到更好的性能。
- 快排花费了很多时间来排序小的子数组,如果用插入排序之类的简单方法实现小数组的排序,程序速度会更快。
不妨令 r - l < cutoff 时采用插入排序,cutoff取值为50比较理想。
- 代码调优时可以将循环体内的swap改为内联代码(另外的swap调用次数少,不计)。
quicksort 5
void quick_sort(int l, int r)
{
if (r - l < cutoff) return;
else
{
swap(l, randint(l,u));
int i = l, j = r + 1, t = x[l];
while(1)
{
while(i <= r && x[i] < t) i++;
while(x[j] > t) j--;
if(i > j)
break;
int temp = x[i];
x[i] = x[j];
x[j] = temp;
}
swap(i,j);
quick_sort(l, j - 1); // 递归调用
quick_sort(j + 1, r);
}
}