排序问题一直都是各类考试和面试的热门问题,在读了《算法之美》第三章后,就发觉其实它在各个场合都很热门。其实我们一直在不断地排序,无论我们做什么。

说回具体的考试面试中的排序,很多人都能用各种语言写出各种排序算法,但很少有人会去思考这些算法步骤背后的秘密,那么本文来抛块砖,说说归并排序和快速排序。

到处都讲归并排序采用分治方法,所以效率比冒泡排序高:

将一个大问题分解成一些小问题,分而治之,各个击破…

但这些诠释都缺了关键点,即 效率必须随规模非线性增加的场景,分治才有意义,如果是线性系统,比如计数,分时调度,分治反而增加额外开销。

只要记住,基于比较的排序效率随着规模下凸增加,就足以理解分治方法了。这很容易理解,每次比较涉及两个数而不是一个,这意味着比较的次数比参与比较的数字规模更大。

把规模缩小一倍,排序效率便不止提升一倍,这是分治策略的基础。排序两个子数组(相当于降维)然后再合并的效率一定比直接排序一个大数组效率高,在此基础之上再看归并排序的时间复杂度。

设待排序数组大小为nnn,使用冒泡排序算法,需要比较的次数为:

T(n)=(n−1)+(n−2)+...+1=n(n−1)2T(n)=(n-1)+(n-2)+...+1=\dfrac{n(n-1)}{2}T(n)=(n−1)+(n−2)+...+1=2n(n−1)​

按照时间复杂度的定义忽略小阶项,T(n)T(n)T(n)可以等价为n2n^2n2。

现在将数组均拆成两半,每一半分别冒泡排序,然后合并两个有序子数组,设TiT_iTi​为将数组均拆成iii个子数组时的排序时间复杂度度量,则:

T2(n)=2(n2)2+nT_2(n)=2(\dfrac{n}{2})^2+nT2​(n)=2(2n​)2+n

具体采用什么算法排序子数组无所谓,只是为了演示经过log⁡n\log nlogn次均拆之后,子数组将仅剩下一个元素,整个归并排序只需要log⁡n\log nlogn层的子数组合并即可,而每层若干合并的总比较次数为nnn。

继续均拆,随着子数组变小,排序涉及的比较次数指数递减。

将大小为iii的子数组逐级合并为大小为2i2i2i的子数组,每次合并操作涉及的操作次数固定为nnn,对于不同的iii,总比较次数分别为:

T4(n)=4(n4)2+2nT_4(n)=4(\dfrac{n}{4})^2+2nT4​(n)=4(4n​)2+2n

T8(n)=8(n8)2+3nT_8(n)=8(\dfrac{n}{8})^2+3nT8​(n)=8(8n​)2+3n

可以期望,子数组大小肯定会递减到1,合并两个大小为1的子数组仅需一次比较,此时:

Tn(n)=n(nn)2+nlog⁡2n=nlog⁡2n+nT_n(n)=n(\dfrac{n}{n})^2+n\log_2n=n\log_2n+nTn​(n)=n(nn​)2+nlog2​n=nlog2​n+n

这就是归并排序的时间复杂度O(nlog⁡2n)O(n\log_2n)O(nlog2​n)。从这个过程发现无论最好,最坏,平均,归并排序的时间复杂度不变。

这是一个非常清晰的过程,但并没有回答一个核心问题,与O(n2)O(n^2)O(n2)的冒泡排序相比,归并排序到底省掉了哪些比较,从而让时间复杂度减少到O(log⁡n)O(\log n)O(logn)?

看一下合并两个已排序数组的过程,请看下图:


两个有序子数组中两组数字,其中一个的头和另一个的尾,在一次合并中不会同时参与比较,减少的比较操作正是来源于此,经过不断分治,归并排序实际上就是一个递归合并的过程。

若仅仅考虑时间复杂度,归并排序可以证明是最高效的排序算法。O(nlog⁡n)O(n\log n)O(nlogn)是比较排序算法绕不过的极限,而这很容易证明。

nnn个数字有n!n!n!种序列,升序(或者降序)是其中一种。nnn个数字中两数经过一次比较可确定两数的相对位置,那么就剩下n!2\dfrac{n!}{2}2n!​种序列,在n!2\dfrac{n!}{2}2n!​种序列中,再比较一次,就会剩下n!4\dfrac{n!}{4}4n!​种序列,以此类推,现在问,至少比较多少次能只剩下1种序列,设比较xxx次可达,那么:

2x≥n!2^x\geq n!2x≥n!

两边取对数,解xxx,即:

x≥log⁡2n!x\geq\log_2n!x≥log2​n!

不等号右边用斯特林公式近似,得到比较排序的时间复杂度极限O(nlog⁡n)O(n \log n)O(nlogn),而归并排序就是比较排序的一种,它的最差情况都能达到O(nlog⁡n)O(n\log n)O(nlogn),这无疑是最棒的算法。

遗憾的是,它的空间复杂度为O(n)O(n)O(n),在实际实现中,随着nnn的增加,排序操作需要大量的内存,这非常不利于cache亲和,而快速排序的原地排序优化可以避免空间问题,这就是快速排序胜出的原因。

那么快速排序为什么可以胜任?它的时间复杂度表现如何呢?

很著名的一篇文章:
数学之美番外篇:快排为什么那样快
我觉得小题大做了,没那么复杂,快速排序核心就是随机,如果每次都能抓到剩余子数组的中位数,每次都是均拆,考虑到每拆到一层iii,该层比较总和为n−1−in-1-in−1−i,总的操作次数是可以算出来的:

T=∑i=0log⁡n(n−1−i)T=\sum\limits_{i=0}^{\log n}(n-1-i)T=i=0∑logn​(n−1−i)

在最好的情况下,快速排序和归并排序时间复杂度均一致,均为O(nlog⁡n)O(n\log n)O(nlogn)。

但每次都抓住中位数几乎不可能,nnn个数选一个,概率均为1n\dfrac{1}{n}n1​,所有子数组均能选中中位数概率可想而知。

无论是否抓到了中位数,快速排序每一步总是可以将子数组分成两个部分,相比O(n2)O(n^2)O(n2)的冒泡排序,快速排序减少比较次数从而降低时间复杂度的秘密在于,在某层被拆开的两部分之间,直到排序结束,均不会发生任何比较。

既然最好情况几乎不可达,最差时间复杂度又是不可接受的O(n2)O(n^2)O(n2),问题是,平均情况下,距离最好情况,差多少呢?如果平均情况接近最好情况,也不错。

快速排序的平均情况显然也是O(nlog⁡n)O(n\log n)O(nlogn),若非如此,也就没有本文,问题是不光要把它推导出来,还要能直观地描绘出来。

平均情况体现在所有排序可能性最接近所有可能性平均数的地方,下图展示了最坏情况到最好情况的单调过渡:

退化成链表的二叉树高度为h=nh=nh=n,它逐渐变矮,直到满二叉树最矮h=log⁡nh=\log nh=logn,这个过程中,hhh越小,可以构造的二叉树越多,可能性分布越密集。而每一棵二叉树都表示一种可能的快速排序,所有可能的平均数位置必然接近密度最大处,显然havgh_{avg}havg​会很小:

有了直观的感受,推导出来的平均时间复杂度就容易理解了。来自wiki,设T(n)T(n)T(n)为排序nnn个数所需的操作:

T(n)=1n∑i=0n−1(T(i)+T(n−i−1))+(n−1)=1.39nlog⁡nT(n)=\dfrac{1}{n}\sum\limits_{i=0}^{n-1}(T(i)+T(n-i-1))+(n-1)=1.39n\log nT(n)=n1​i=0∑n−1​(T(i)+T(n−i−1))+(n−1)=1.39nlogn

其中(n−1)(n-1)(n−1)为每一次排列子数组固定的比较操作。这就定量化了上图中平均值的位置,偏离最好情况39%处,非常近。

这就是快速排序为什么在统计意义上很快的原因。这是随机的胜利,虽然达到最好情况概率非常低,但统计意义上绝大多数情况离最好情况都不太远,这是随机的魅力!

相比之下,归并排序显得有些呆板。

随机似乎与理性相反,但在某些问题上,它却比最好的确定性算法更优秀。

浙江温州皮鞋湿,下雨进水不会胖。

归并排序与快速排序背后的秘密相关推荐

  1. 归并排序,快速排序为什么快

    对于一个 n n n个元素的数组,必须要确定两两之间的相对顺序,假设每次都抓取不同的二元组,需要 log ⁡ 2 n ! \log_2n! log2​n!次比较,由于 log ⁡ 2 n ! ≈ n ...

  2. CC讲坛-大脑疾病背后的秘密-许执恒

    <CC讲坛>第二十期于2017年7月27日在北京东方梅地亚中心M剧场举行,中国科学院遗传与发育生物学研究所研究员许执恒出席并进行题为<大脑疾病背后的秘密>的演讲. 胚胎时期大脑 ...

  3. 排序算法中——归并排序和快速排序

    冒泡排序.插入排序.选择排序这三种算法的时间复杂度都为 $O(n^2)$,只适合小规模的数据.今天,我们来认识两种时间复杂度为 $O(nlogn)$ 的排序算法--归并排序(Merge Sort)和快 ...

  4. 云计算背后的秘密(6)-NoSQL数据库的综述

    我本来一直觉得NoSQL其实很容易理解的,我本身也已经对NoSQL有了非常深入的研究,但是在最近准备YunTable的Chart的时候,发现NoSQL不仅非常博大精深,而且我个人对NoSQL的理解也只 ...

  5. 云计算背后的秘密(1)-MapReduce

    之前在IT168上已经写了一些关于云计算误区的文章,虽然这些文章并不是非常技术,但是也非常希望它们能帮助大家理解云计算这一新浪潮,而在最近几天,IT168的唐蓉同学联系了我,希望我能将云计算背后的一些 ...

  6. 排序算法:归并排序、快速排序

    相关博客: 排序算法:冒泡排序.插入排序.选择排序.希尔排序 排序算法:归并排序.快速排序 排序算法:桶排序.计数排序.基数排序 排序算法:堆排序 十大排序算法小结 一.归并排序: 1.工作原理: 归 ...

  7. 排序算法--(冒泡排序,插入排序,选择排序,归并排序,快速排序,桶排序,计数排序,基数排序)

    一.时间复杂度分析 - **时间复杂度**:对排序数据的总的操作次数.反应当n变化时,操作次数呈现什么规律 - **空间复杂度**:算法在计算机内执行时所需要的存储空间的容量,它也是数据规模n的函数. ...

  8. if快还是switch快?解密switch背后的秘密

    这是我的第 57 篇原创文章 条件判断语句是程序的重要组成部分,也是系统业务逻辑的控制手段.重要程度和使用频率更是首屈一指,那我们要如何选择 if 还是 switch 呢?他们的性能差别有多大?swi ...

  9. 十大排序算法:冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序、计数排序、桶排序、基数排序

    冒泡排序.选择排序.插入排序.希尔排序.归并排序.快速排序.堆排序.计数排序.桶排序.基数排序的动图与源代码. 目录 关于时间复杂度 冒泡排序 选择排序 插入排序 希尔排序 归并排序 快速排序 堆排序 ...

最新文章

  1. 6 个理由,让我不顾一切撑腰 Python!
  2. java中executorservice_java中ExecutorService创建方法总结
  3. Machine Learning week 9 quiz: Anomaly Detection
  4. 经典C语言程序100例之五三
  5. Spring面试问题
  6. linux的搜索和时间
  7. cognos10 安装部署
  8. codesys file读写配置参数程序
  9. apesv100数据库_生物信息学相关数据库资源介绍..ppt
  10. 使用腾讯云OCR文字识别
  11. 2.证券投资基金的概述
  12. 未来15年,还有一波“增量”机会
  13. MTFCSGO准心设置
  14. c语言中3%3e2%3e1的值,Javascript中的空数组值
  15. OverNet-250FPS SISR实时算法- | Lightweight Multi-Scale Super-Resolution with Overscaling Network
  16. java迷宫生成代码_maxe.java 源代码在线查看 - Java Maze 计算机自动生成迷宫 资源下载 虫虫电子下载站...
  17. 计算机专业要不要考研——写的很棒
  18. Simulink仿真---Park变换、反Park变换
  19. 如何让Echarts地图只显示某个省、市、区
  20. 通证经济,一个正在狂奔的互联网数字经济时代

热门文章

  1. 微信红包封面的N种玩儿法
  2. react 配合 react-draggable 进行拖拽
  3. 抓systrace的常用的四种方法
  4. 【京东飞天茅台1499抢购】Python 脚本的完整安装、使用教程与解决方案
  5. MAC上Pycharm破解导致无法打开
  6. roku能不能安装软件_如何在Roku上更改家长控制PIN
  7. Yangtze worknote
  8. 黑鸟linux课程讲义,品牌形象设计课程课件讲义.doc
  9. element-ui 表单渲染v-if组件,验证报错
  10. 打造个人IP的六个步骤