一、什么是归并排序

归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,通过把问题递归分解成子问题,最后到数组长度为一后自下而上回归解决问题,达成排序目标。

二、归并排序实现

1 分治法

在讲具体实现之前,首先要讲一个设计算法的方法,就是分治法。

很多好的算法在结构上是递归的,通过将问题递归分解成若干个子问题从而达成解决问题的目的,这些算法遵循分治法的思想。

分治法在每层递归遵循三个步骤:

  1. 将问题分解成若干子问题,这些子问题是规模相对小的原问题。
  2. 解决这些子问题,这里有两种情况,若子问题还是较大则继续分解成子问题,否则直接求解。
  3. 不断合并子问题的解形成原问题。
2 基本实现思路

归并排序遵循了分治法的思想,其步骤为:

  1. 将规模为n的数组分解成规模为n/2的数组。
  2. 若子数组的规模不为1,则继续分解问题,否则直接求解。
  3. 不断合并已排完序的子数组以获得排完序的数组。

假设有一数组

5 , 2 , 4 , 6 , 1 , 3 , 8 , 7 {5,2,4,6,1,3,8,7} 5,2,4,6,1,3,8,7​

分解​

将其分解为两个n/2数组,也就是

{ 5 , 2 , 4 , 6 } { 1 , 3 , 8 , 7 } \{5,2,4,6\}\ \ \{1,3,8,7\} {5,2,4,6}  {1,3,8,7}​

子数组规模不唯一,继续

{ 5 , 2 } { 4 , 6 } { 1 , 3 } { 8 , 7 } \{5,2\}\ \{4,6\}\ \ \{1,3\}\ \{8,7\} {5,2} {4,6}  {1,3} {8,7}​​

子数组规模不唯一,继续

{ 5 } { 2 } { 4 } { 6 } { 1 } { 3 } { 8 } { 7 } \{5\}\{2\}\ \{4\}\{6\}\ \ \{1\}\{3\}\ \{8\}\{7\} {5}{2} {4}{6}  {1}{3} {8}{7}​

分解过程

合并

1-2

子数组规模为1,直接求解,由于长度为1不需要排序,所以进入合并状态,合并数组长度1为2

{ 5 } \{5\} {5}

{ 2 } \{2\} {2}​​

将上述两个合并只需比较一次5大于2,获得

{ 2 , 5 } \{2,5\} {2,5}

同理,得到

{ 2 , 5 } { 4 , 6 } { 1 , 3 } { 7 , 8 } \{2,5\}\ \{4,6\}\ \ \{1,3\}\ \{7,8\} {2,5} {4,6}  {1,3} {7,8}​

2-4

上一步合并进入了每个数组2个数字状态,进一步合并前一半

{ 2 , 5 } \{2,5\} {2,5}

{ 4 , 6 } \{4,6\} {4,6}

把这两个数组想象成一个队列,这两个队列从低到高排序完毕,因此,你只需不断对比第一个元素,就可获取最小值

2小于4,所以2最小,所以2的位置确定

{ 2 , , , } \{2,,,\} {2,,,}​

剩余队列为

{ 5 } \{5\} {5}​

{ 4 , 6 } \{4,6\} {4,6}

4小于5,所以

{ 2 , 4 , , } \{2,4,,\} {2,4,,}

剩余队列为

{ 5 } \{5\} {5}​

{ 6 } \{6\} {6}

5小于6,所以排序完毕

{ 2 , 4 , 5 , 6 } \{2,4,5,6\} {2,4,5,6}

同理可以完成下一组

{ 1 , 3 , 7 , 8 } \{1,3,7,8\} {1,3,7,8}

4-8

此时有两个队列

{ 2 , 4 , 5 , 6 } \{2,4,5,6\} {2,4,5,6}

{ 1 , 3 , 7 , 8 } \{1,3,7,8\} {1,3,7,8}

按照上述步骤不断对比第一个元素,可以得到

1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 {1,2,3,4,5,6,7,8} 1,2,3,4,5,6,7,8

排序完成

伪代码实现

归并排序一共分两步,一是分解,二是合并。

当我们对比两个数组时,若有下面这种情况

1 , 2 , 3 , 4 1,2,3,4 1,2,3,4

5 , 6 , 7 , 8 5,6,7,8 5,6,7,8

我们只需对比4次,第一个数组就没了,由于下面数组最小的数字大于上面的数组,且下面数组处于顺序的状态,因此,只需要将数组2按顺序复制到临时数组中即可。

mergeSort(arr[],start,end) // 获取数组、初始值下标和末尾值下标arrLength = end - start + 1 // 获取数组长度
mid = (startIndex + endIndex) / 2 // 获取中间点// 若问题规模不为1,递归调用自己, 否则向上回归
if end > startmergeSort(arr[], start, mid)mergeSort(arr[], mid + 1, end)
elsereturn
endif// 合并
temp[arrLength] // 定义临时存储数组
temp1 = 0, temp2 = 0, countNum = 0 // 临时变量,temp1和temp2代表当前最前面的那个数字是谁
temp1Num = mid - start + 1, temp2Num = end - mid // 计算合并时,需合并的两个数组长度,当(5  2)合并时均为1for i = 0 to arrLengthif temp1 < temp1Num and temp2 < temp2Num // 若两个队列都还有数字if arr[start + temp1] <= arr[mid + 1 + temp2] //对比首数字temp[countNum] = arr[startIndex + temp1] //放入临时数组++temp1 //送走第一个数组第一个 elsetemp[countNum] = arr[midIndex + 1 + temp2]++temp2 //送走第二个数组第一个 endif ++countNum // 记录临时数组记录到哪里了elseif temp1 == temp1Num // 第一个数组已经走完// 把第二个数组剩余的数按顺序放入临时数组for j = temp2 to temp2Num-1temp[countNum] = arr[mid + 1 + temp2]++temp2++countNumbreakendforelseif temp2 == temp2Num// 同理for j = temp1 to temp1Num-1temp[countNum] = arr[mid + 1 + temp1]++temp1++countNumbreakendforendif// 将排完序的数组复制到原数组for i = 0 to arrLength-1arr[start + i] = temp[i];endfor

quiz :当我们的数组不是2n时,如上面数组长度为7时会发生什么,我们的代码还有效吗 ?(答案是有效的,有疑问的可以思考并尝试一下)

关注公众号 愿人人如龙 回复归并排序即可免费领取c++代码(代码非常简陋,仅供学习使用),如有错误,非常感谢你通知我。

三、算法分析

1 空间复杂度

在归并排序中,我们需要把问题向下分解,当我们拆分问题是,我们不需要额外的空间,我们只是不断分解数组的索引。

但当我们向上合并时,由于逐步对比首数字的方式无法做到原址排序(会破坏对比过程),因此需要额外的空间来储存排序好的数字。


由上图可以看到,每分解一层,需要n个存储空间,那接下来就是计算分解多少层。

有方程 2 x = n , x 2^x = n,\ x 2x=n, x 为层数,因此 x = l g n x=lgn x=lgn​ ;

由此得,我们需要 n l g n nlgn nlgn​个额外存储空间,因此,归并排序空间复杂度为 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn)​。​

2 时间复杂度

对于归并排序这样一个递归的问题,我们可以这样描述

T ( n ) = { Θ ( 1 ) n = 1 2 T ( n 2 ) + f ( n ) n ≠ 1 T(n)=\left\{ \begin{array}{c} \Theta(1) & n=1 \\ \displaystyle 2T(\frac{n}{2})+f(n) & n\ne1\\ \end{array} \right. T(n)={Θ(1)2T(2n​)+f(n)​n=1n​=1​​​​​

首先,我们认为当规模为1时,问题的时间复杂度为常数,也就是 Θ ( 1 ) \Theta(1) Θ(1)​。

其次, f ( n ) f(n) f(n) 为分解问题和合并问题的消耗。

对于归并排序,分解只需计算中间值,花费时间为常数 c 1 c_1 c1​,合并时需要比较最多 n n n次,复制 n n n次,记为 c 2 n ( 1 < c 2 < 2 ) c_2n(1<c_2<2) c2​n(1<c2​<2)总计为 c 1 + c 2 n c_1+c_2n c1​+c2​n​​​

因此对于 规模为n的数组,总计 ( c 1 + c 2 n ) l g n (c_1+c_2n)lgn (c1​+c2​n)lgn,时间复杂度也就是 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn)。

归并排序的时间复杂度为 Θ ( n l g n ) \Theta(nlgn) Θ(nlgn),而插入排序和冒泡排序为 Θ ( n 2 ) \Theta(n^2) Θ(n2)​,初学算法的同学可能对这两者差别没有概念,那么我们通过计算来观察一下。

当规模 n = 100 n=100 n=100​​ 时, n 2 = 10000 n^2=10000 n2=10000​​ , n l g n ≈ 664 nlgn\approx 664 nlgn≈664​​ ,差别约为​15倍;

当规模 n = 10000 n=10000 n=10000​​​ 时, n 2 = 1 × 1 0 8 n^2=1\times10^8 n2=1×108​​​ , n l g n ≈ 132877 nlgn\approx 132877 nlgn≈132877​​​ ,差别约为752倍;

当规模 n = 1000000 n=1000000 n=1000000​​​​​ 时, n 2 = 1 × 1 0 12 n^2=1\times10^{12} n2=1×1012​​​​​ , n l g n ≈ 2 × 1 0 7 nlgn\approx 2\times10^7 nlgn≈2×107​​​​​ ,差别约为10万倍;

也就是说当排序一百万个数时,冒泡排序可能要花近归并排序10万倍的时间,当然这不准则,因为时间复杂度进行了大量的简化并隐藏了系数和低阶项,但这仍然能表明算法的相对时间复杂度。

我用自己电脑测试了归并排序和插入排序所需的时间:

插入排序排序10万个数花了7秒,100万个数字花了600秒。

归并排序排序100万个数字花了不到1秒,1000万个数字花了7秒。

这个例子充分展现了算法的魅力。

3 主定理

可以不看,描述不完整且比较抽象,但是一种可以通过套公式快速计算递归问题的时间复杂度的方法。

注定理给出了形式为

T ( n ) = a T ( n / b ) + f ( n ) T(n) = aT(n/b)+f(n) T(n)=aT(n/b)+f(n)

这种问题的简便的时间复杂度求解方法,下面我将介绍这个定理常见情况的使用(不进行证明,有兴趣可以去看书查资料)。

令 a ≥ 1 a\ge1 a≥1 和 b > 1 b>1 b>1 是常数, f ( n ) f(n) f(n) 是一个非负函数。有递归式:

T ( n ) = { Θ ( 1 ) n = 1 a T ( n b ) + f ( n ) n = b i T(n)=\left\{ \begin{array}{c} \Theta(1) & n=1 \\ \displaystyle aT(\frac{n}{b})+f(n) & n=b^i\\ \end{array} \right. T(n)={Θ(1)aT(bn​)+f(n)​n=1n=bi​​​

其中 i i i 为正整数,则 T ( n ) T(n) T(n) 的时间复杂度为

  1. 若对某个常数 ε > 0 \varepsilon>0 ε>0​ 有 f ( n ) = O ( n l o g b a − ε ) \displaystyle f(n)=O(n^{log_b{a-\varepsilon}}) f(n)=O(nlogb​a−ε)​ ,则 T ( n ) = Θ ( n l o g b a ) T(n) = \Theta(n^{log_b^{a}}) T(n)=Θ(nlogba​)​​。
  2. 若 f ( n ) = O ( n l o g b a ) f(n)=O(n^{log_b{a}}) f(n)=O(nlogb​a)​​​​ ,则 T ( n ) = Θ ( n l o g b a l g n ) T(n) = \Theta(n^{log_b{a}}lgn) T(n)=Θ(nlogb​algn)​​​​​。

这里并没有完整的描述这个定理,仅写出我觉得的常用情况。

对于这个定理的解释:

将问题分解成若干个所节省的时间与分解合并的时间对比

若 f ( n ) = n x f(n) = n^x f(n)=nx, 若 x < l o g b a x<{log_b{a}} x<logb​a,则 T ( n ) = Θ ( n l o g b a ) T(n) = \Theta(n^{log_b^{a}}) T(n)=Θ(nlogba​)

若 x = l o g b a x={log_b{a}} x=logb​a​,则 T ( n ) = Θ ( n l o g b a l g n ) T(n) = \Theta(n^{log_b{a}}lgn) T(n)=Θ(nlogb​algn)​。

例子

1

对于上述的归并排序, a = b = 2 , x = 1 a=b=2,x=1 a=b=2,x=1​, f ( n ) = n f(n) = n f(n)=n​

l o g 2 2 = x = 1 log_22=x=1 log2​2=x=1

所以满足第二条, T ( n ) = Θ ( n l o g 2 2 l g n ) = Θ ( n l g n ) T(n) = \Theta(n^{log_2{2}}lgn)=\Theta(nlgn) T(n)=Θ(nlog2​2lgn)=Θ(nlgn)。

2

对于 T ( n ) = 7 T ( n / 2 ) + Θ ( n 2 ) T(n) = 7T(n/2)+\Theta(n^2) T(n)=7T(n/2)+Θ(n2)​​

a = 7 , b = 2 , x = 2 a=7,b=2,x=2 a=7,b=2,x=2​

l o g 2 7 ≈ 2.8 > x = 2 log_27 \approx 2.8>x = 2 log2​7≈2.8>x=2​, 因此满足第一条, T ( n ) = Θ ( n l o g 2 7 ) ≈ Θ ( n 2.8 ) T(n) = \Theta(n^{log_2^{7}})\approx \Theta(n^{2.8}) T(n)=Θ(nlog27​)≈Θ(n2.8)​​ 。

算法1.4.归并排序相关推荐

  1. 图解排序算法(四)之归并排序

    图解排序算法(四)之归并排序 基本思想 归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide) ...

  2. python选择排序算法图解_python基本算法之实现归并排序(Merge sort)

    0.前言 评判一个算法的好坏的标准: 时间复杂度 空间复杂度 1.归并排序算法是什么? 冒泡排序(Bubble Sort)是一种建立在归并操作上面的一种有效的排序算法,由John von neuman ...

  3. python 归并排序算法_python基本算法之实现归并排序(Merge sort)

    0.前言 评判一个算法的好坏的标准: 时间复杂度 空间复杂度 1.归并排序算法是什么? 冒泡排序(Bubble Sort)是一种建立在归并操作上面的一种有效的排序算法,由John von neuman ...

  4. 归并python_python基本算法之实现归并排序(Merge sort)

    0.前言 评判一个算法的好坏的标准: 时间复杂度 空间复杂度 1.归并排序算法是什么? 冒泡排序(Bubble Sort)是一种建立在归并操作上面的一种有效的排序算法,由John von neuman ...

  5. js排序算法详解-归并排序

    js系列教程5-数据结构和算法全解 js排序算法详解-归并排序 归并排序其实可以类比二分法,二分法其实就是二等分的意思,简而言之就是不断和新序列的中间值进行比较.归并排序似乎有异曲同工之妙,什么意思呢 ...

  6. java 排序算法总结,Java排序算法总结之归并排序

    本文实例讲述了Java排序算法总结之归并排序.分享给大家供大家参考.具体分析如下: 归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作.和快速排序类似,让我们一起来看 ...

  7. Hark的数据结构与算法练习之归并排序

    算法说明: 归并排序的思路就是分而治之,将数组中的数字递归折半进行排序. 递归到最底层就只剩下有两个数字进行比较,再从底层往下进行排序合并.最终得出结果. 同样,语言描述可能对于不知道这个算法的人来说 ...

  8. 排序算法系列:归并排序算法

    概述 上一篇我们说了一个非常简单的排序算法--选择排序.其复杂程序完全是冒泡级的,甚至比冒泡还要简单.今天要说的是一个相对比较复杂的排序算法--归并排序.复杂的原因不仅在于归并排序分成了两个部分进行解 ...

  9. 算法模板:归并排序【沈七】

    本文已收录于专栏 ⭐️ <算法通关笔记>⭐️ 算法模版:归并排序 前言 基本概念 算法思想 常用模板 完结散花 参考文献 前言 唤我沈七就好. 往期专栏: 算法模板:快速排序 基本概念 归 ...

  10. 排序算法之——二路归并排序

    排序算法之--二路归并排序 二路归并排序的思想: 一次排序过程,将已经各自有序的两个段的数据合并一个段,并且合并后依旧有序 开始,我们认为单个数据是有序的,一个数据就是一个段,一次排序之后,两个数据就 ...

最新文章

  1. 利用Comet4J 及时推送消息
  2. 【Spring学习】spring开发包介绍
  3. Twitch如何实现转码比FFmpeg性能提升65%?(下)
  4. 【面试相关】python实现快速幂取余算法详解
  5. 中南月赛 1313: ZZY的宠物
  6. 去除代码行号的一个小程序(控制台版本)
  7. mosaic数据增强_YoloV4当中的Mosaic数据增强方法(附代码详细讲解)
  8. WebService 常用免费调用接口 与 JWS(Java Web Service) 调用第三方 webService 天气服务
  9. 算法设计与分析期末复习题(史上最详细)
  10. 左程云算法 哈希函数
  11. 华为平均每天收入23.5亿元!重磅发布2019年年报!
  12. 服务器维护合同需要交印花税吗,服务合同需要交印花税吗
  13. 【JavaWeb】最详细的小白笔记!!!
  14. 启动优化之一——启动分析及优化方案
  15. 电脑、Windows系统下方搜索栏搜不出文件怎么办?如何解决?实测有效
  16. hbase 使用lzo_装配HBase LZO
  17. 计算机网络技术基础 阚宝明,计算机网络技术基础阚宝明答案
  18. Linux rz命令安装失败解决方法
  19. C++并查集算法(详细)
  20. Google Java Style 中文版

热门文章

  1. 还在用 open 读文件?out 了,这个库比 open 好用 100 倍
  2. 2千万用户APP的开发运营流程
  3. 流量主开通以及添加广告步骤
  4. 从开发的角度看待产品需求评审会
  5. 一个计算机爱好者的不完整回忆(三十八)我的手机
  6. fabricjs 中自定义控件图标
  7. JasperReport、iReport以及JavaBean绑定表格动态赋值
  8. 数据链路层协议的三个基本问题
  9. 基于SSM框架的超市进销存管理系统
  10. Java异常处理:SSL证书异常:SSLHandshakeException: sun.security.validator.ValidatorException