快速排序的时间复杂度为O(nlogn),空间复杂度为O(logn)最坏情况下的时间复杂度为O(n2),空间复杂度O(n)。在C++标准库的实现过程中,为了保证快速排序的速度,用堆排序对快速排序做了优化,即使在最坏情况下,sort仍然可以维持O(nlogn)的时间复杂度,空间复杂度则不会超过O(logn)。在一般性应用场合,快速排序不会愧对它的名字,尽管时间复杂度为O(nlogn),但常数因子很小,因此排序速度比其它时间复杂度为O(nlogn)的堆排序和归并排序要快一些,甚至超越了理论上比它快的基数排序和计数排序以及桶排序,这后三种排序的时间复杂度理论值可是O(n)哦。C++继承了C库,因此C库里的qsort也被继承下来,只不过sort要比qsort排序速度快得多。
对于一个无需序列而言,当我们需要把它变成一个升序序列时,可以把它一分为二,使左边的子序列都小于或等于某个值,右边的子序列都大于等于某个值,左右子序列继续这种划分,直到整个序列有序为止。这是最原始的快速排序设计思想,它是分治算法的应用。
现在的问题是,拿什么作为基准把一个序列划分成两个子序列呢?弄不好一边只有一个元素,另一边则为其余所有元素,这种划分过程一直持续到排序完毕为止,这是最糟糕的情况,完全退化成了直接插入排序,复杂度为O(n2)。这个参考的基准,也就是序列里用于比较的某个特别的元素,就是支点,这个词来自力学。这样以来,选取支点就成了快速排序首当其冲要解决的问题。
选取支点常见的方案有三种,第一种情况是使用待排序序列的第一个元素或最后一个元素作为支点。采用这种方案的人非常乐观,以为最坏情况很难遇上。然而事情很多时候总是事与愿违,当一个待排序序列已经有序时就会遭遇最差情况。第二种方案是三点取中,以待排序序列中第一个元素,最中间那个元素和最后一个元素三者大小居中的那个作为支点,这就大大降低了遭遇最差情况的可能性,而且三点取中的代价很低。实际应用中,几乎所有版本的快速排序都采用了这种方案。第三种方案是随机选取支点,似乎这是最理想的情况,但是该方案一直搁浅。
显然,第一种选取支点的方案不值得我们去研究,我们先来尝试第二种选取支点的方案,稍后再尝试第三种方案。前面已经说过,当序列很短或基本有序时用直接插入排序比较快。我们可以考虑用直接插入排序来优化快速排序,否则当子序列少于三个元素时选取支点需要做特殊处理,再者,用递归来实现快速排序较为简单,而深层次递归的执行效率很低。
经过优化后三点取中快速排序过程示例如下: 27 98 96 96 40 79 91 86 29 85 56 72 45 85 60 12 14 92 97 26 89 三点取中
27 26 96 96 40 79 91 86 29 85 56 72 45 85 60 12 14 92 97 98 89 交换98和26
27 26 14 96 40 79 91 86 29 85 56 72 45 85 60 12 96 92 97 98 89 交换96和14
27 26 14 12 40 79 91 86 29 85 56 72 45 85 60 96 96 92 97 98 89 交换96和12
27 26 14 12 40 45 91 86 29 85 56 72 79 85 60 96 96 92 97 98 89 交换79和45
27 26 14 12 40 45 56 86 29 85 91 72 79 85 60 96 96 92 97 98 89 交换91和56
27 26 14 12 40 45 56 29 86 85 91 72 79 85 60 96 96 92 97 98 89 交换86和29
12 14 26 27 29 40 45 56 60 72 79 85 85 86 89 91 92 96 96 97 98 插入排序
原始的快速排序使用直接插入排序优化后,速度有了较大提升,但是最坏情况下退化成直接插入排序的诟病依然存在。解决这个问题需要用较低的代价获知排序过程中何时支点严重偏向一边,以便立即作出处理。这个难题被STL的另外一位大师David Musser在1997年解决,他使用监测递归深度的做法,当递归深度很大时说明支点严重偏斜,此时采用堆排序来保证O(nlogn)的时间复杂度。SGI STL里的sort采用了这种设计。
第三种选取支点的方案的原理非常简单,但是长期以来存在较大困难。首先,用软件产生随机数不是一件很容易的事,实际应用中只能用产生的伪随机数取代随机数,而伪随机数的质量和产生代价会影响快排的执行效率。这可能是长期以来一直没有人问津第三种选取支点方案的主要原因。再者,随机选取的支点未必十分适合需要排序的序列。在此,我们不做深入研究伪随机数的产生原理,以及随机选取的支点在多大程度上适合待排序的序列,因为二者均需要十分复杂的数学理论。