小智最近迷上了计算机算法,今天过来给大家讲讲排序算法。

准备讲排序算法之前,我们还是要先回顾一下排序这个概念。

排序是一门古老的科学。排序问题,用数学的方式可以表达如下

问题输入:给定n个数,a1,  a2,  a3, ..., an
要求输出:给出n个数的排列,a1', a2', a3', ..., an',使得 a1' ≤ a2' ≤ a3' ≤ ... ≤ an'

从更形象的角度来说,排序就是一群人站成一列,高的站前面,矮的站在后面。

一个家庭的所有成员按身高排列的示意图(小智实在找不到图了,画一个示意-_-)

关于排序,有几个描述算法特征的词语:

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;

不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;

时间复杂度:一个算法执行所耗费的时间。

空间复杂度:运行完一个程序所需内存的大小。

ok,了解完排序的相关概念以后,我有一个新的问题了:如何评价一个算法?小智认为,可以用一个词语来形容一个优秀的算法:多快好省

什么是“多快好省”?我们从算法的角度来解析一下:

:能够可靠处理大规模的数据

:算法的时间复杂度要更低的

:算法实现要符合稳定性

:算法的空间复杂度要更低的

到底哪些算法能够符合“多快好省”这个目标呢?我们先看一下目前的算法分类:

基于比较的排序算法的特征总结

注:本文不讨论非基于比较算法,比如计数排序、桶排序和基数排序

“多”的角度:从数据量的处理性能来看,需要不受随机因素影响的算法。冒泡算法、插入排序、快速排序这三个算法最好情况和最坏情况相差了一个数量级,这种不可靠性可能影响数据处理的效率。

“快”的角度:从时间复杂度的角度来看,希尔排序、归并排序、快速排序、堆排序的算法复杂度比较好,均低于O(n2)。

“好”的角度:从稳定性的角度来看,归并排序、插入排序、冒泡排序是稳定的

“省”的角度:从空间复杂度的角度来看,冒泡排序、插入排序、希尔排序、堆排序,空间复杂度为O(1)。其中,值得注意的是,一般写法的归并排序,空间复杂度为O(n),经过优化以后,使用空间可以为O(1)。

大家都看出了我的倾向了吧?小智从“多快好省”这四个方面,分析发现,归并算法在这四个方面表现不错。这次小智决定跟大家探讨一下归并算法

为什么归并排序能够做到多快好省?

解答这个问题之前,我们先了解一下什么是归并排序。

归并算法(merge sort)是一个分治算法(divide and conquer algorithm),冯·诺依曼(John von Neumann )在1945年发明了这个算法。这个算法将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。

实际上,归并算法的算法步骤非常简单:

第1步:拆分,把长度为n的输入序列分成两个长度为n/2的子序列

第2步:递归调用,对这两个子序列分别使用归并排序

第3步:合并,将两个排序好的子序列合并成一个最终的排序序列

这几步,我们来分别地讲一下:

什么是拆分?将一个长序列拆分成两个子序列。这个属于分治法中的“分”,将大问题降解为小问题。

将长序列拆分成两个子序列示意图

如果n为奇数,不能被2整除,可以将n/2向上取整,这个对算法没有影响。

有一个特殊情况是需要注意的:当子序列长度为1时,已经没有办法进行拆分了,可以直接执行下一步。

什么是递归调用?实际上是重复调用归并排序的程序。我们假想一下,在最开始的情况下,执行完第1步,长序列被拆分为2个子序列。接着执行第二步时,直接对子序列进行归并排序,那么下一步则是继续拆分子序列。如此下去,直至将长序列拆分到无法分拆的情形。

我们以图来说明会更加清晰一点:

归并算法递归调用的分拆效果

经过不断地递归调用,要处理的子序列长度越来越短,最后直至子序列长度为1。

有一个特殊情况是需要注意的:当子序列长度为1时,已经不需要继续调用归并排序了,因此可以直接跳过递归调用这个步骤。

什么是合并?是将两个排好序的子序列,合成一个也是排好序的序列。这个合并算法也是比较简单。

合并过程可以表述为:对于两个子序列,X和Y,其中X和Y都是升序排列,以及知道X和Y每个数据在原来长序列的位置,分别为Xp和Yp,如何将X和Y合并成一个稳定的升序排列?

由于这两个子序列都是升序排列,因此我们同时遍历这两个子序列,从遍历的位置各拿出一个数字,哪个数字小就把它拿出来插入合并序列中。如果遇到拿出来的两个数字相等的情况,则比较原来序列的位置,哪个小就把它拿出来即可。

按照上述合并步骤,我们可以写出合并伪代码了:

i = 1, j = 1, l = 1

申请一个临时变量temp,用于储存合并后序列

如果 X[i] < Y[j]: temp[l] = X[i], l = l + 1, i = i + 1

如果 X[i] < Y[j]: temp[l] = Y[j], l = l + 1, j = j + 1

如果 X[i] = Y[j]:

如果 Xp[i] < Yp[j]: temp[l] = X[i], l = l + 1, i = i + 1

如果 Xp[i] > Yp[j]: temp[l] = Y[j], l = l + 1, j = j + 1

我将合并流程补充到整个归并算法的执行流程当中,以图的形式做了一个示例:

归并排序整体流程示意图

归并排序我们了解完了,我们可以开始回答为什么归并排序能够表现如此出色:

“多”的角度:归并排序为什么能保证可靠地处理数据?

归并排序的拆分的流程,跟数据的原始排序没有关系,无论是最坏情况还是最好情况,时间复杂度都是一致的,能够稳定处理大量数据。


“快”的角度:归并排序的时间复杂度为什么是 O(n log n)?

设归并排序所消耗的时间为T(n),进行归并排序的拆分操作以后,将原来的问题划分为原来规模的二分之一,每一个划分出来的问题将耗费时间是T(n/2),最后把这两部分有序的数组合并在一起所花的时间为O(n),因此:

T(n) = 2×T(n/2) + O(n)

而划分次数最多可以有logn次,因此累加起来可得T(n) = O(n log n)

“好”的角度:归并排序为什么能保证稳定呢?


归并排序将序列分割到最小,而后将序列进行合并。假如两个数字是相等的,在分割子序列的时候,不会更改他们之间的相对位置;在合并子序列的时候,只要能够做到先将位置在前的数字放到合并数组,这样就保证了排序结果的稳定。

“省”的角度:归并排序为什么空间复杂度可以为O(1)?

归并排序虽然原始写法的确是这样的,但是算法是可以改造的,我们只需要做到原地排序就可以了,而对于归并排序而言,原地排序的关键是合并。归并排序的原来写法,当进行合并时,是申请一个临时空间,将合并的数据放到临时空间当中,这个算法复杂度为 O(n) 

实际上,合并过程可以直接使用已有的位置。假设我们要合并的两段数组还是[13, 23, 37, 54]和[11,17, 26,29],在归并算法里,它们可以看成一个连续的数组形式:

[13, 23, 37, 54, 11, 17, 26, 29]

首先我们检查两段数组的第一个数,发现13 > 11,那么11这个数要拿出来,原来是要放到临时空间的第一个位置,但我们可以利用数组已有的位置,将11直接移到数组的第一个位置,然后原来第一段数组往后移一个位置,这样数组变为

[11, 13, 23, 37, 54, 17, 26, 29]

而后我们继续按照这种方式,合并[13, 23. 37. 54]和[17, 26 29]这两段数组。

可以发现,这种方式空间复杂度只需一个临时变量,用于协助数字移动,因此空间复杂度为O(1)。当然这样优化增加了移动的次数,为了空间,就要损失时间了。

总结一下,归并排序是个宝,一个多快好省的排序算法,大家如果遇到数据排序问题,可以优先考虑它。当然,归并排序并不是全能的,在某些类型问题下,有些算法比归并排序表现更为出色,往后小智会给大家解读。

你知道怎么样排序才能做到多快好省?相关推荐

  1. MySQL排序原理与MySQL5.6案例分析【转】

    本文来自:http://www.cnblogs.com/cchust/p/5304594.html,其中对于自己觉得是重点的加了标记,方便自己查阅.更多详细的说明可以看沃趣科技的文章说明. 前言   ...

  2. 排个课表学会了拓扑排序!有点意思

    原创不易,帅哥美女呢请三连支持一波 前言 大家好,我是bigsai. 拓扑排序,很多人都可能听说但是不了解的一种算法.不知者大多会提出这样的疑问: 这是某种排序算法?这好像是一种图论算法?图也能排序? ...

  3. DirectX11 With Windows SDK--27 计算着色器:双调排序

    前言 上一章我们用一个比较简单的例子来尝试使用计算着色器,但是在看这一章内容之前,你还需要了解下面的内容: 章节 26 计算着色器:入门 深入理解与使用缓冲区资源(结构化缓冲区/有类型缓冲区) Vis ...

  4. mysql 排序字段索引吗_Mysql 排序优化与索引使用(转)

    为了优化SQL语句的排序性能,最好的情况是避免排序,合理利用索引是一个不错的方法.因为索引本身也是有序的,如果在需要排序的字段上面建立了合适的索引,那么就可以跳过排序的过程,提高SQL的查询速度.下面 ...

  5. 【java】java 分支预测 Java处理排序后的数组比没有排序的快

    1.概述 今天周日,没什么重要的事情要做,于是我早早的就醒来了.看了一会渡边淳一的书,内心逐渐感到平静--心情不佳的时候,书好像是最好的药物.心情平静了,就需要做一些更有意义的事情--逛技术网站,学习 ...

  6. mysql排序优化_Mysql 排序优化

    为了优化SQL语句的排序性能,最好的情况是避免排序,合理利用索引是一个不错的方法.因为索引本身也是有序的,如果在需要排序的字段上面建立了合适的索引,那么就可以跳过排序的过程,提高SQL的查询速度.下面 ...

  7. 为什么?为什么?Java处理排序后的数组比没有排序的快?想过没有?

    先看再点赞,给自己一点思考的时间,微信搜索[沉默王二]关注这个有颜值却假装靠才华苟且的程序员. 本文 GitHub github.com/itwanger 已收录,里面还有我精心为你准备的一线大厂面试 ...

  8. MySql基础篇---002 SQL之SELECT使用篇: 基本的SELECT语句,运算符,排序与分页,多表查询,单行函数,聚合函数,子查询

    第03章_基本的SELECT语句 讲师:尚硅谷-宋红康(江湖人称:康师傅) 官网:http://www.atguigu.com 1. SQL概述 1.1 SQL背景知识 1946 年,世界上第一台电脑 ...

  9. 「Java」- 八大排序

    目录 前言 1.冒泡排序 2.选择排序 3.插入排序 4.希尔排序 5.堆排序 6.快速排序 7.归并排序 8.计数排序 前言 由于本章介绍的大多数排序都需要用到数组两个元素之间进行交换操作 , 所以 ...

最新文章

  1. dbcp 连接池参数说明
  2. python控制nao机器人_python控制nao机器人身体动作实例详解
  3. 发现一个好工具RenderDoc
  4. 如何使用Dev C++调试(debug)c程序
  5. eclipse/myeclipse中如何开启断言调试代码
  6. 【Android 修炼手册】Gradle 篇 -- Gradle 源码分析
  7. go ip过滤_用Go实现自己的爬虫
  8. [.net基础]访问修饰符
  9. 您知道数据中心的最佳温度是多少吗?
  10. 数字孪生数据中心机房,智能 IDC 高阶运维
  11. python怎么设置为中文-python如何设置中文界面
  12. 计算逆矩阵的三种方法
  13. word文档如何插入目录
  14. 在安装win7系统时如何不产生100M的系统保留分区
  15. 计算机桌面机器人,桌面的 Spirit Rover(勇气号探测器),机器人爱好者的心头好!!...
  16. python文字游戏攻防_一个简单的孙悟空斗牛魔王的文字游戏
  17. java excel 数组公式_教你如何快速学习Excel数组公式及运用
  18. Ubound Lbound,数组上下边界 将二维数组处理后写入新数组,并粘贴到单元格中。
  19. 【PaddleHub模型贡献】一行代码实现水表的数字表盘分割
  20. dede修改描述description限制字数长度

热门文章

  1. 同时支持EF+Dapper的混合仓储,助你快速搭建数据访问层
  2. 基于Asp.Net Core MVC和AdminLTE的响应式管理后台之侧边栏处理
  3. 一张图带你了解 Insider Dev Tour 2019中国技术大会
  4. dotnet core 微服务教程
  5. Docker最全教程——从理论到实战(九)
  6. Asp.NET Core2.0 项目实战入门视频课程_完整版
  7. spring cloud+.net core搭建微服务架构:Api授权认证(六)
  8. Oleans集群之Consul再解释
  9. 在Docker中运行asp.net core 跨平台应用程序
  10. python maketrans_Python maketrans()方法 - Python 教程 - 自强学堂