首页 > 代码库 > 快速排序为什么快?
快速排序为什么快?
这是曾经思考过的问题,它为什么叫快速排序呢?思考无果,然后忘记了,然后昨天被问起,自然想不出很好的答案。直到,看到了《暗时间》上有这个问题的答案。
在《暗时间》里,作者刘未然并没有直接给出答案,而是先说了两个游戏,猜数字和称球。这两个问题都很好理解,并且不难解答。然而,令我豁然开朗的是,他们指向了同一个思想,分而治之!把问题不断切割一半又一半,直到答案水落石出。
回到正题,我们的目标是排序,无论哪个排序方法都是基于两两比较的,问题在于如何才能减少比较的次数呢?举个例子,有这么一组数:1,2,3,4,5,15,78,89,90,100,200;一共11个。并且给出的初始顺序是从小到大的,现在要排成从大到小。快速排序的思想,就是从中抽取一个数(称为基准吧),然后大于基准的在一边,小于或等于在另一边。比如,现在随机的抽取了78,那么1,2,3,4,5,15会在一边,89,90,100,200会在另一边。这时候,注意到,从这一刻开始,小于78的那些数就再也没有机会与大于78的数进行两两比较了。快速排序用了分而治之的思想,虽然,由于随机抽取,我们最好第一次抽到的是15,这样就平分了。但是没关系。忽略掉这个随机性因素,快排还是把大问题分成了两个小问题,哪怕这两个字问题不一定对等。只要递归下去,结果水到渠成。
相反,我们看看同样是理论上与快排一样的时间复杂度(O(nlogn))的堆排序,就说最大堆吧,它把最大的元素移除后,把最后的叶子结点拿上来,是为了重建堆。但是,明显,拿上的值是要比它的两个叶子结点要小很多的。它要比较很多次,才能回到合适的位置。并且,我发现,在父结点的值与子树结点的值比较前,左子树和右子树要先比较大小,然后拿出最大的那个才去跟父节点去比较大小。
两次两两比较,才换来一次有意义的交换,如此循环下去,堆排序做了很多无用功!
以上,从逻辑上分析了快速排序为什么比归并排序和堆排序快的原因。但,这还是不够的。接着,从数学统计上分析他们的时间复杂度——
(1)堆排序——
在构建堆的过程中,因为我们是完全二叉树从最下层最右边的非终端结点开始构建,将它与其孩子进行比较和若有必要的互换,对于每个非终端结点来说,其实最多进行两次比较和互换操作,因此整个构建堆的时间复杂度为O(n)。
在正式排序时,第i次取堆顶记录重建堆需要用O(logi)的时间(完全二叉树的某个结点到根结点的距离为.log2i.+1),并且需要取n-1次堆顶记录,因此,重建堆的时间复杂度为O(nlogn)。
(2)归并排序——
假如用T(n)表示使用归并排序对n个元素构成的数组进行排序而使用的时间,用mergeTime来表示将两个子分组合并起来而花费的时间。那么
T(n) = T(n/2)+T(n/2) + mergetime
而megeTime就是归并两个子数组所耗费的时间,以最大时间来算,最多需要n-1次来比较两个子数组的元素,然后n次移动到临时数组中。那么mergetime就是2n -1 。
因此 T(n) = T(n/2)+ T(n/2) +2n -1 。
(3)快速排序——
在最差的情况下,要把n个元素的数组划分,需要n次比较和n次移动。假设用T(n)来表示使用快速排序算法来排序n个元素的数组所耗费的时间。那么
T(n) = T(n/2)+ T(n/2) +2n
如此看来,有人可能就想到了,从数学公式上看,归并排序还比快排要少个1呢,是不是要快些?其实,不是这样的,请注意到那几个字"最差的情况下",就是在我每次选择的基准,取决于输入的基准,都是最不理想的基准,恰好要移动最多次才能达到目的。但是这种情况的出现的概率是1/(2^n),非常非常小,而且,这是最普通的快速排序方法,先人提出了很多改进优化的方案,其中一种被很常用的就是采用随机函数来选择基准来避免最坏的情况的发生。
在畅销的ACM信息竞赛教材《算法艺术与信息学竞赛》上,作者也提出了关于快排的效率与优化问题——
正如我们所分析的,一旦我们采用随机函数来产生基准,那么时间复杂度计算公式里,
T(n) = T(n/2)+ T(n/2) +2n 里2n就不再是2n了,也许是1,也许是2...但是,最后的结果肯定是要比2n小很多了。
至于堆排序,明显,堆排序主要是堆调整的时间很耗时,90%的时间耗在堆调整上。因此在数据量大的时候,明显是要落后很多的。
至此,我想我明白了为什么快排比其他两种都要快了,因为它善于分解子问题,从不做无用功。虽然有最糟糕的情况,但是这个是可以最大限度降低其出现概率的。有不同意见的,欢迎拍砖,谢谢。