快速排序算法的优化思路总结

写于2016年1月11日,若有错漏,欢迎斧正。javascript

原文前端

前两天在知乎上看到了一个关于快速排序算法性能的问题,我简单总结了一个优化思路,如今在本身的博客里也贴一下吧,版权都是个人。java

其实里面的大部份内容在个人另外一篇博客里有讲过:深刻了解javascript的sort方法python

原回答:www.zhihu.com/question/39…算法

快速排序水很深啊。我不贴代码,主要讲讲优化思路和手段吧。数组

1. 合理选择pivot

你就直接选择分区的第一个或最后一个元素作 pivot 确定是不合适的。对于已经排好序,或者接近排好序的状况,会进入最差状况,时间复杂度衰退到 O(N^2)缓存

pivot选取的理想状况是:让分区中比 pivot 小的元素数量和比 pivot 大的元素数量差很少。较经常使用的作法是三数取中( midian of three ),即从第一项、最后一项、中间一项中取中位数做为 pivot。固然这并不能彻底避免最差状况的发生。因此不少时候会采起更当心、更严谨的 pivot 选择方案(对于大数组特别重要)。好比先把大数组平均切分红左中右三个部分,每一个部分用三数取中获得一个中位数,再从获得的三个中位数中找出中位数。性能

我在 javascript v8 引擎中看到了另一种选择 pivot 的方式:认为超过1000项的数组是大数组,每隔200左右(不固定)选出一个元素,从这些元素中找出中位数,再加入首尾两个元素,从这个三个元素中找出中位数做为 pivot。优化

By the way,现实环境中,你要对一个预先有必定顺序的数组作排序的需求太太太广泛了,这个优化必需要有。ui

2. 更快地分区

我看到题主的作法是从左向右依次与 pivot 比较,作交换,这样作其实效率并不高。举个简单的例子,一个数组 [2, 1, 3, 1, 3, 1, 3],选第一个元素做为 pivot,若是按题主的方式,每次发现比2小的数会引发一次交换,一共三次。然而,直观来讲,其实只要将第一个3和最后一个1交换就能够达到这三次交换的效果。因此更理想的分区方式是从两边向中间遍历的双向分区方式。实现的话你能够参考楼上 @林面包的实现。

3. 处理重复元素的问题

假如一个数组里的元素所有同样大(或者存在大量相同元素),会怎么样?这是一个边界 case,可是会令快速排序进入最差状况,由于无论怎么选 pivot,都会使分区结果一边很大一边很小。那怎么解决这个问题呢?仍是修改分区过程,思路跟上面说的双向分区相似,可是会更复杂,咱们须要小于 pivot、等于 pivot、大于 pivot 三个分区。既然说了不贴代码,那就点到为止吧,有兴趣能够本身找别人实现看看。

4. 优化小数组效率

这一点不少人都提到了。为何要优化小数组?由于对于规模很小的状况,快速排序的优点并不明显(可能没有优点),而递归型的算法还会带来额外的开销。因而对于这类状况能够选择非递归型的算法来替代。好,那就有两个问题:多小的数组算小数组?替换的算法是什么?

一般这个阈值设定为16( v8 中设定的是10),替换的算法通常是选择排序。听说阈值的设定是要考虑更好地利用 cpu 缓存,这个问题我就不是很清楚了,不深刻。一样,对于分区获得的小数组是要马上进行选择排序,仍是等分区所有结束了以后,再统一进行选择排序,这个问题也会存在必定的缓存命中的区别,我也不懂,不深刻。

5. 监控递归过程

这里我要说的是内省排序。想一想看,咱们已经作了一些努力来避免快速排序算法进入最坏的状况。但事实上可能并不如咱们想象地那么理想。理想状况下,快速排序算法的递归尝试会到多深呢?这个很好回答:\log{N}。好,若是如今递归深度已经到了 \log{N},我会以为很正常,毕竟不太可能每次都是最好状况嘛;那若是此时递归深度达到 2\times\log{N} 呢?我想你应该慌了,比理想状况递归深了一倍尚未结束。而此时,我以为能够认为可能已经进入最差状况了,继续使用快速排序只会更遭,能够考虑对这个分区采用其余排序算法来处理。一般咱们会使用堆排序。为啥要用堆排序?由于它的平均和最差时间复杂度都是 O(N\log{N})。这就是内省排序的思想。

6. 优化递归

我想先说明一点:内省排序虽然会避免递归过深,但它的目的并非为了优化递归。

在分区过程当中,咱们实际上是把一个大的问题分解成两个小一点的问题分别处理。这时咱们须要考虑一下,这两个小问题哪一个更小。先处理更小规模的问题,再处理更大规模的问题,这样能够减少递归深度,节约栈开销。

楼上也有人提到了尾递归。对于支持尾递归的语言,天然是极好的,小规模的问题先递归,减小递归深度,大规模的问题直接经过尾递归优化掉,不进入递归栈。

然而并非全部的语言都支持尾递归 ⊙︿⊙,好比 python(听说)和 javascript。在 javascript 的 v8 引擎中,我看到它是用一个循环变相手动实现了一个与尾递归效果同样的优化,棒棒哒。

7. 并行

既然快速排序算法是典型的分治算法,那么对于分解下来的小问题是能够在不一样的线程中并行处理的。固然对于 javascript 仍是不适用,嗯,我是作前端的。