快速排序
本节的主题是快速排序,它可能是应用最广泛的排序算法了。快速排序流行的原因是它实现简单、适用于各种不同的输人数据且在一般应用中比其他排序算法都要快得多。快速排序引人注目的特点包括它是原地排序(只需要一个很小的辅助栈),且将长度为\(N\)的数组排序所需的时间和\(MlgN\)成正比。我们已经学习过的排序算法都无法将这两个优点结合起来。另外,快速排序的内循环比大多数排序算法都要短小,这意味着它无论是在理论上还是在实际中都要更快。它的主要缺点是非常脆弱,在实现时要非常小心才能避免低劣的性能。已经有无数例子显示许多种错误都能致使它在实际中的性能只有平方级别。幸好我们将会看到,由这些错误中学到的教训也大大改进了快速排序算法,使它的应用更加广泛。
基本算法
快速排序是一种分治的排序算法。它将一个数组分成两个子数组,将两部分独立地排序。快速排序和归并排序是互补的:归并排序将数组分成两个子数组分别排序,并将有序的子数组归并以将整个数组排序;而快速排序将数组排序的方式则是当两个子数组都有序时整个数组也就自然有序了。在第一种情况中,递归调用发生在处理整个数组之前;在第二种情况中,递归调用发生在处理整个数组之后。在归并排序中,一个数组被等分为两半;在快速排序中,切分( partition )的位置取决于数组的内容。快速排序的大致过程如下图所示。
快速排序
1 | public class Quick { |
快速排序递归地将子数组a[lo..hi]排序,先用partition()方法将a[j]放到一个合适的位置,然后再用递归调用将其他位置的元素排序。
该方法的关键在于切分,这个过程使得数组满足下面三个条件:
对于某个j,a[j] 已经排定;
a[lo]到a[j-1]中的所有元素都不大于a[j];
a[j+1]到a[hi]中的所有元素都不小于a[j]。
我们就是通过递归地调用切分来排序的。
快速排序的切分
1 | //将数组切分为a[lo..j-1],a[i],a[j+1..hi] |
之后还有些讨论的内容这里不再详写,请看原著!
性能特点
数学上已经对快速排序进行了详尽的分析,因此我们能够精确地说明它的性能。大量经验也证明了这些分析,它们是算法调优时的重要工具。
快速排序切分方法的内循环会用一个递增的索引将数组元素和一个定值比较。这种简洁性也是快速排序的一个优点,很难想象排序算法中还能有比这更短小的内循环了。例如,归并排序和希尔排序一般都比快速排序慢,其原因就是它们还在内循环中移动数据。
快速排序另一个速度优势在于它的比较次数很少。排序效率最终还是依赖切分数组的效果,而这依赖于切分元素的值。切分将一个较大的随机数组分成两个随机子数组,而实际上这种分割可能发生在数组的任意位置(对于元素不重复的数组而言)。
下面我们来分析这个算法,看看这种方法和理想方法之间的差距。看原著!
算法改进
快速排序是由C.A.R Hoare在1960年发明的,从那时起就有很多人在研究并改进它。改进快速排序总是那么吸引人,发明更快的排序算法就好像是计算机科学界的“老鼠夹子”,而快速排序就是夹子里的那块奶酪。几乎从Hoare第-次发 表这个算法开始,人们就不断地提出各种改进方法。并不是所有的想法都可行,因为快速排序的平衡性已经非常好,改进所带来的提高可能会被意外的副作用所抵消。但其中一些,也是我们现在要介绍的,非常有效。
如果你的排序代码会被执行很多次或者会被用在大型数组上(特别是如果它会被发布成一个库函数,排序的对象数组的特性是未知的),那么下面所讨论的这些改进意见值得你参考。需要注意的是,你需要通过实验来确定改进的效果并为实现选择最佳的参数。一般来说它们能将性能提升20% ~ 30%。
以下三小结略
切换到插入排序
三取样切分
熵最优的排序
三向切分的快速排序
1 | public class Quick3way { |
参考资料
《算法(第4版)》第二章排序2.3快速排序