声明:图片来自于《算法》第四版。本文章需要实际的遇到各种问题,才会体会到里面的内容。希望大家静下心来,好好思考。这对思维会有很大的提示。也可以提高对算法中隐藏缺陷的发现能力有一个很好的提升。
快速排序可能是应用最广泛的排序算法了,快速排序流行的原因是它实现简单,适用于各种不同的输入数据且在一般应用中比其他算法都要快的多。
快速排序引人注目的三点:
1、 原地排序(只需要一个很小的辅助栈),
2、 长度为N的数组排序所需要的时间和NlgN成正比。
3、 快排的内循环比其他大多数排序算法都要短小,这意味着无论是在理论还是实际中都要更快
缺点:
非常脆弱,要非常小心才能避免低劣的性能。本次讲解也会关注其中错误可能发生的地方。
基本算法
快速排序是一种分治的排序算法,将一个数组分成两个子数组,将两部分独立地进行排序。
快速排序和归并排序是互补的:
a、 归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将数组排序,而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了,
b、 归并排序递归调用发生在处理整个数组之前;快速排序递归调用发生在处理整个数组之后。
c、 在归并排序中,一个数组被切分为两半;在快速排序中切分的位置取决于数组的内容。
快速排序示意图
//快速排序 public class Quick{
public static void sort(Comparable[] a){
StdRndom.shuffle(a); //消除对输入的依赖 sort(a,0,a.length-1);
}
private static void sort(Comparable[] a, int lo, int hi){
if(hi <= lo) return;
int j = partition(a,lo,hi); //寻找切分点 sort(a, lo, j-1);
sort(a, j+1,hi);
}
}
该方法的关键在于切分,整个过程使数组满足以下条件
a、 对于某个j,a[j]已经排定;(j的选择越靠近中央,排序越快)
b、 a[lo]到a[j-1]中的所有元素都不大于a[j];
c、 a[j+1]到a[hi]中的所有元素都不小于a[j];
通过递归调用切分来排序。
实现切分方法:
一般策略是
a、 随意地取a[lo]作为切分元素。
b、 然后从数组的左端开始向右扫描直到找到一个大于等于它的元素,
c、 再从数组的右端开始向左扫描直到找到一个小于等于它的元素,
d、 交换这两个元素的位置。
e、 如此继续。
f、 当两指针相遇时,只需将切分元素a[lo]和左子树的最右侧元素(a[j])交换然后返回j即可。
//快速排序的切分
private static int partition(Comparable[] a, int lo, int hi){ //将数组切分为a[lo..i-1],a[i],a[i+1..hi]
int i = lo, j = hi +1;
Comparable v = a[lo];
while(true){ //扫描左右,检查扫描是否结束并交换元素
while (less(a[++i], v)) if (i == hi) break;
while (less(v, a[--j])) if (j ==lo) break;
if( i >= j) break;
each(a, i, j);
}
each(a, lo , j); //将v = a[j] 放入正确的位置
return j;
}
切分轨迹
值得注意的几个细节问题
a、 原地切分
如果使用一个辅助数组,我们可以很容易实现切分,但将切分后的数组复制回去的开销也许使我们得不偿失。这回大大降低排序的速度。
b、 别越界
如果切分元素是数组中最小或最大的那个元素,我们就要小心别让扫描指针跑出数组的边界partition()实现可进行明确的检测来预防这种情况。测试条件(j == lo)是冗余的,因为切分元素就是a[lo],它不可能比自己小。数组右端也有相同的情况,它们都是可以去掉的。
c、 保持随机性
数组元素的顺序是被打乱的,因为该算法对所有子数组一视同仁,它的所有子数组也都是随机排序的。这对于预测算法的运行时间很重要。
d、 终止循环
保证循环结束需要格外小心,正确检测指针是否越界需要一点技巧。
e、 处理切分元素值有重复的情况
如算法所示,左侧扫描最好是在遇到大于等于切分元素值的元素停下,右侧扫描则是在遇到小于等于切分元素值的元素停下。尽管这样可能会不必要的将一些等值元素交换(不稳定算法),但是在某些典型应用中它能够避免算法的运行时间变为平方级。
f、 终止递归
切分元素正好是数组的最大或最小值时会陷入无限的递归循环中。
算法改进:
a、 切换到插入排序
和大多数递归排序算法一样,改进排序性能的一个简单办法基于以下两点:
对于小数组,快速排序比插入排序慢
因为递归,快速排序的sort()方法在小数组中也会调用自己。
b、 三取样切分
使用子数组的一小部分元素的中位数来切分数组。这样做的切分更好,但代价就是需要计算中位数。人们发现将取样大小设为3并用大小居中的元素切分效果最好。
c、 熵最优的排序
针对于数组中还有大量重复元素。
将数组切分为三部分,分别对应小于,等于和大于切分元素的数组
//三向切分的快速排序
public class Quick3way{
private static void sort(Comparable[] a, int lo, int hi){
if(hi <= lo) return;
int lt = lo, i = lo+1, gt = hi;
Comparable v = a[lo];
while( i <= gt) {
int cmp = a[i].comparaeTo(v);
if (cmp < 0) exch(a, lt++, i++);
else if (cmp > 0) exch(a , i , gt--);
else i++;
} //现在a[lo..lt-1] < v = a[lt..gt] < a[gt+1..hi]成立
sort(a, lo, lt - 1);
sort(a, gt + 1, hi);
}
}
三向切分的轨迹
快速排序是第一批偏爱随机性的算法。
经过精心调优的快速排序在绝大多数计算机上的绝大多数应用中都会比其他基于比较的排序算法更快。