分类目录:​​《算法设计与分析》总目录​

​​​


快速排序的运行时间依赖于划分是否平衡,而平衡与否又依赖于用于划分的元素。如果划分是平衡的,那么快速排序算法性能与归并排序一样。如果划分是不平衡的,那么快速排序的性能就接近于插入排序了。在本文中,我们将给出划分为平衡或不平衡时快速排序性能的非形式化的分析。

最坏情况划分

当划分产生的两个子问题分别包含了 n − 1 n-1 n−1个元素和 0 0 0个元素时,快速排序的最坏情况发生了。不妨假设算法的每一次递归调用中都出现了这种不平衡划分。划分操作的时间复杂度是 Θ ( n ) \Theta(n) Θ(n)。由于对一个大小为 0 0 0的数组进行递归调用会直接返回,因此 T ( 0 ) = Θ ( 1 ) T(0)=\Theta(1) T(0)=Θ(1),于是算法运行时间的递归式可以表示为: T ( 0 ) = T ( n − 1 ) + T ( 0 ) + Θ ( n ) = T ( n − 1 ) + Θ ( n ) T(0)=T(n-1)+T(0)+\Theta(n)=T(n-1)+\Theta(n) T(0)=T(n−1)+T(0)+Θ(n)=T(n−1)+Θ(n)从直观上来看,每一层递归的代价可以被累加起来,从而得到一个算术级数,其结果为 Θ ( n 2 ) \Theta(n^2) Θ(n2)。因此,如果在算法的每一层递归上,划分都是最大程度不平衡的,那么算法的时间复杂度就是 Θ ( n 2 ) \Theta(n^2) Θ(n2)。也就是说,在最坏情况下,快速排序算法的运行时间并不比插入排序更好。此外,当输入数组已经完全有序时,快速排序的时间复杂度仍然为 Θ ( n 2 ) \Theta(n^2) Θ(n2)。而在同样情况下,插入排序的时间复杂度为 O ( n ) O(n) O(n)。

最好情况划分

在可能的最平衡的划分中, 我们得到的两个子问题的规模都不大于 n 2 \frac{n}{2} 2n。在这种情况下,快速排序的性能非常好。此时,算法运行时间的递归式为 T ( n ) = 2 T ( n 2 ) + O ( n ) T(n)=2T(\frac{n}{2})+O(n) T(n)=2T(2n)+O(n)在上式中,我们忽略了一些余项以及减1操作的影响。根据主定理的情况,上述递归式的解为 T ( n ) = Θ ( n lg ⁡ n ) T(n)=\Theta(n\lg n) T(n)=Θ(nlgn)。通过在每一层递归中都平衡划分子数组,我们得到了一个渐近时间上更快的算法。

平衡的划分

快速排序的平均运行时间更接近于其最好情况,而非最坏情况。详细的分析可以参看​​《排序算法(七):快速排序-[快速排序的分析]》​​。理解这一点的关键就是理解划分的平衡性是如何反映到描述运行时间的递归式上的。

例如,假设划分算法总是产生 9 : 1 9:1 9:1的划分,此时,我们得到的快速排序时间复杂度的递归式为:

T ( n ) = T ( 9 n 10 ) + T ( n 10 ) + c n T(n) = T(\frac{9n}{10})+T(\frac{n}{10})+cn T(n)=T(109n​)+T(10n​)+cn这里,我们显式地写出了 Θ ( n ) \Theta(n) Θ(n)项中所隐含的常数 c c c。下图显示了这一递归调用所对应的递归树。注意,树中每一层的代价都是 c n cn cn,直到在深度 log ⁡ 9 10 n = Θ ( log ⁡ n ) \log_{\frac{9}{10}}{n}=\Theta(\log n) log109​​n=Θ(logn)处达到递归的边界条件时为止,之后每层代价至多为 c n cn cn。递归在深度为 log ⁡ 9 10 n = Θ ( log ⁡ n ) \log_{\frac{9}{10}}{n}=\Theta(\log n) log109​​n=Θ(logn)处终止。因此,快速排序的总代价为 O ( n log ⁡ n ) O(n\log n) O(nlogn)。因此,即使在递归的每一层上都是 9 : 1 9:1 9:1的划分,直观上看起来非常不平衡,但快速排序的运行时间是 O ( n log ⁡ n ) O(n\log n) O(nlogn),与恰好在中间划分的渐近运行时间是一样的。实际上,即使是 99 : 1 99:1 99:1的划分,其时间复杂度仍然是 O ( n log ⁡ n ) O(n\log n) O(nlogn)。事实上,任何一种常数比例的划分都会产生深度为 O ( n log ⁡ n ) O(n\log n) O(nlogn)的递归树,其中每一层的时间代价都是 O ( n ) O(n) O(n)。因此,只要划分是常数比例的,算法的运行时间总是 O ( n log ⁡ n ) O(n\log n) O(nlogn)。

算法设计与分析——排序算法(七):快速排序-[快速排序的性能]_数据结构

对于平均情况的直观观察

为了对快速排序的各种随机情况有一个清楚的认识,我们需要对遇到各种输入的出现频率做出假设。快速排序的行为依赖于输入数组中元素的值的相对顺序,而不是某些特定值本身。

当对一个随机输入的数组运行快速排序时,想要像前面非形式化分析中所假设的那样,在每一层上都有同样的划分是不太可能的。我们预期某些划分会比较平衡,而另一些则会很不平衡。在平均情况下, ​​partition(arr,low,high)​​​所产生的划分同时混合有“好”和“差”的划分。此时,在与​​partition(arr,low,high)​​平均情况执行过程所对应的递归树中,好和差的划分是随机分布的。基于直觉,假设好和差的划分交替出现在树的各层上,并且好的划分是最好情况划分,而差的划分是最坏情况划分,下图中左图显示出了递归树的连续两层上的划分情况。在根结点处,划分的代价为 n n n,划分产生的两个子数组的大小为 n − 1 n-1 n−1和 0 0 0,即最坏情况。在下一层上,大小为 n − 1 n-1 n−1的子数组按最好情况划分成大小分别为 n − 1 2 − 1 \frac{n-1}{2}-1 2n−1​−1和 n − 1 2 \frac{n-1}{2} 2n−1​的子数组。在这里,我们假设大小为 0 0 0的子数组的边界条件代价为1。在一个差的划分后面接着一个好的划分,这种组合产生出三个子数组,大小分别为 0 0 0、 n − 1 2 − 1 \frac{n-1}{2}-1 2n−1​−1和 n − 1 2 \frac{n-1}{2} 2n−1​。这一组合的划分代价为 Θ ( n ) \Theta(n) Θ(n)。该代价并不比下图中右图中的更差。

在下图中右图中,一层划分就产生出大小为 n − 1 2 \frac{n-1}{2} 2n−1​的两个子数组,划分代价为 Θ ( n ) \Theta(n) Θ(n)。从直观上看,差划分的代价 Θ ( n − 1 ) \Theta(n-1) Θ(n−1)可以被吸收到好划分的代价 Θ ( n ) \Theta(n) Θ(n)中去,而得到的划分结果也是好的。因此,当好和差的划分交替出现时,快速排序的时间复杂度与全是好的划分时一样,仍然是 O ( n log ⁡ n ) O(n\log n) O(nlogn)。区别只是 O O O符号中隐含的常数因子要略大一些。

算法设计与分析——排序算法(七):快速排序-[快速排序的性能]_算法_02