快速排序可能是应用最广泛的排序算法了。原因是它实现简单,适用于各种不同的输入数据且在一般应用中比其他排序算法都要快得多。
特点:①它是原地排序(只需要一个很小的辅助栈);②且将长度为N的数组排序所需时间和NlgN成正比。
与归并排序的联系与区别:
相同点:都是一种分治的排序算法,都将一个数组分成两个子数组。
不同点:①归并排序是等分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序是当两个子数组都有序时,整个数组也就自然有序了(切分位置取决于数组的内容)。
快速排序函数实现如下:
public class Quick{
public static void sort(Comparable [] a) {
StdRandom.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);//将右半部分排序
}
}
快速排序的划分
public static int partition(Comparable [] a,int lo,int hi) {
//将数组切分
int i=lo,j=hi+1;//设置左右扫描指针
Comparable v=a[lo];//将数组第一个数a[lo]设置为切分元素v
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);//交换i和j指向的元素
}
each(a,lo,j);//将a[lo]和j最后指向的元素交换位置,已知j指向的元素都是小于切分元素,即a[lo]的。
return j;//达成a[lo...j-1]<=a[j]<=a[j+1..hi]
}
分析:将数组第一个值设为切分元素,用i指针从左向右扫描,j指针从右往左扫描。当i扫描到大于切分元素的值,停止移动并指向它;当j扫描到小于切分元素的值,停止移动并指向它。交换i和j指向的元素。当i>=j或者i、j越界时停止,最后交换切分元素和j指针指向的元素,真正实现,切分元素左侧比它小,右侧比它大。
注意:①别越界:如果切分元素是数组中最小或者最大的那个元素,我们就要防止指针跑出数组边界。可以通过哨兵改进。
②保持随机性:切分元素的选择对算法性能影响很大,保持随机性可以尽量大的可能性选择合适切分元素。
③终止循环:一个常见错误是没有考虑到数组中可能包含和切分元素的值相同的其他元素。
④处理切分元素值有重复的情况:左侧扫描改为小于等于,右侧扫描改为大于等于。尽管这样可能会不必要的将一些等值的元素交换,但是他能够避免算法运行时间变为平方级别。
性能:快速排序切分方法的内循环会用一个递增的索引将数组元素和一个定值比较。这种简洁性也是快速排序的一个优点。例如,归并排序和希尔排序一般都比快速排序慢,就是因为他们还在内循环中移动数据。
将长度为N的无重复数组排序,快速排序平均需要2NlnN次比较(以及1/6的交换)。
快速排序最多需要约N²/2次比较,但随即打乱数组能够预防这种情况。
算法改进:
①切换到插入排序:改进使遇到小数组时使用插入排序,将if(hi<=lo)return;替换成if(hi<=lo+M){Insertion,sort(a,lo,hi);return;}。转换参数的最佳值适合系统相关的,但是5~15之间的任意值在大多数情况下都能够令人满意。
②三取样切分
③熵最优的排序:三向切分的快速排序。我们已经知道归并排序是最优的,如何突破他的下界?这个问题的答案讨论的是对于任意输入的最差性能,而我们目前在讨论时已经知道输入数组的一些信息了。对于含有以任意概率分布的重复元素的输入,归并排序无法保证最佳性能。