算法1.4.归并排序
一、什么是归并排序
归并排序(Merge Sort)是建立在归并操作上的一种有效,稳定的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,通过把问题递归分解成子问题,最后到数组长度为一后自下而上回归解决问题,达成排序目标。
二、归并排序实现
1 分治法
在讲具体实现之前,首先要讲一个设计算法的方法,就是分治法。
很多好的算法在结构上是递归的,通过将问题递归分解成若干个子问题从而达成解决问题的目的,这些算法遵循分治法的思想。
分治法在每层递归遵循三个步骤:
- 将问题分解成若干子问题,这些子问题是规模相对小的原问题。
- 解决这些子问题,这里有两种情况,若子问题还是较大则继续分解成子问题,否则直接求解。
- 不断合并子问题的解形成原问题。
2 基本实现思路
归并排序遵循了分治法的思想,其步骤为:
- 将规模为n的数组分解成规模为n/2的数组。
- 若子数组的规模不为1,则继续分解问题,否则直接求解。
- 不断合并已排完序的子数组以获得排完序的数组。
假设有一数组
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) c2n(1<c2<2)总计为 c 1 + c 2 n c_1+c_2n c1+c2n
因此对于 规模为n的数组,总计 ( c 1 + c 2 n ) l g n (c_1+c_2n)lgn (c1+c2n)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) 的时间复杂度为
- 若对某个常数 ε > 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(nlogba−ε) ,则 T ( n ) = Θ ( n l o g b a ) T(n) = \Theta(n^{log_b^{a}}) T(n)=Θ(nlogba)。
- 若 f ( n ) = O ( n l o g b a ) f(n)=O(n^{log_b{a}}) f(n)=O(nlogba) ,则 T ( n ) = Θ ( n l o g b a l g n ) T(n) = \Theta(n^{log_b{a}}lgn) T(n)=Θ(nlogbalgn)。
这里并没有完整的描述这个定理,仅写出我觉得的常用情况。
对于这个定理的解释:
将问题分解成若干个所节省的时间与分解合并的时间对比
若 f ( n ) = n x f(n) = n^x f(n)=nx, 若 x < l o g b a x<{log_b{a}} x<logba,则 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=logba,则 T ( n ) = Θ ( n l o g b a l g n ) T(n) = \Theta(n^{log_b{a}}lgn) T(n)=Θ(nlogbalgn)。
例子
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 log22=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)=Θ(nlog22lgn)=Θ(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 log27≈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.归并排序相关推荐
- 图解排序算法(四)之归并排序
图解排序算法(四)之归并排序 基本思想 归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide) ...
- python选择排序算法图解_python基本算法之实现归并排序(Merge sort)
0.前言 评判一个算法的好坏的标准: 时间复杂度 空间复杂度 1.归并排序算法是什么? 冒泡排序(Bubble Sort)是一种建立在归并操作上面的一种有效的排序算法,由John von neuman ...
- python 归并排序算法_python基本算法之实现归并排序(Merge sort)
0.前言 评判一个算法的好坏的标准: 时间复杂度 空间复杂度 1.归并排序算法是什么? 冒泡排序(Bubble Sort)是一种建立在归并操作上面的一种有效的排序算法,由John von neuman ...
- 归并python_python基本算法之实现归并排序(Merge sort)
0.前言 评判一个算法的好坏的标准: 时间复杂度 空间复杂度 1.归并排序算法是什么? 冒泡排序(Bubble Sort)是一种建立在归并操作上面的一种有效的排序算法,由John von neuman ...
- js排序算法详解-归并排序
js系列教程5-数据结构和算法全解 js排序算法详解-归并排序 归并排序其实可以类比二分法,二分法其实就是二等分的意思,简而言之就是不断和新序列的中间值进行比较.归并排序似乎有异曲同工之妙,什么意思呢 ...
- java 排序算法总结,Java排序算法总结之归并排序
本文实例讲述了Java排序算法总结之归并排序.分享给大家供大家参考.具体分析如下: 归并操作(merge),也叫归并算法,指的是将两个已经排序的序列合并成一个序列的操作.和快速排序类似,让我们一起来看 ...
- Hark的数据结构与算法练习之归并排序
算法说明: 归并排序的思路就是分而治之,将数组中的数字递归折半进行排序. 递归到最底层就只剩下有两个数字进行比较,再从底层往下进行排序合并.最终得出结果. 同样,语言描述可能对于不知道这个算法的人来说 ...
- 排序算法系列:归并排序算法
概述 上一篇我们说了一个非常简单的排序算法--选择排序.其复杂程序完全是冒泡级的,甚至比冒泡还要简单.今天要说的是一个相对比较复杂的排序算法--归并排序.复杂的原因不仅在于归并排序分成了两个部分进行解 ...
- 算法模板:归并排序【沈七】
本文已收录于专栏 ⭐️ <算法通关笔记>⭐️ 算法模版:归并排序 前言 基本概念 算法思想 常用模板 完结散花 参考文献 前言 唤我沈七就好. 往期专栏: 算法模板:快速排序 基本概念 归 ...
- 排序算法之——二路归并排序
排序算法之--二路归并排序 二路归并排序的思想: 一次排序过程,将已经各自有序的两个段的数据合并一个段,并且合并后依旧有序 开始,我们认为单个数据是有序的,一个数据就是一个段,一次排序之后,两个数据就 ...
最新文章
- 利用Comet4J 及时推送消息
- 【Spring学习】spring开发包介绍
- Twitch如何实现转码比FFmpeg性能提升65%?(下)
- 【面试相关】python实现快速幂取余算法详解
- 中南月赛 1313: ZZY的宠物
- 去除代码行号的一个小程序(控制台版本)
- mosaic数据增强_YoloV4当中的Mosaic数据增强方法(附代码详细讲解)
- WebService 常用免费调用接口 与 JWS(Java Web Service) 调用第三方 webService 天气服务
- 算法设计与分析期末复习题(史上最详细)
- 左程云算法 哈希函数
- 华为平均每天收入23.5亿元!重磅发布2019年年报!
- 服务器维护合同需要交印花税吗,服务合同需要交印花税吗
- 【JavaWeb】最详细的小白笔记!!!
- 启动优化之一——启动分析及优化方案
- 电脑、Windows系统下方搜索栏搜不出文件怎么办?如何解决?实测有效
- hbase 使用lzo_装配HBase LZO
- 计算机网络技术基础 阚宝明,计算机网络技术基础阚宝明答案
- Linux rz命令安装失败解决方法
- C++并查集算法(详细)
- Google Java Style 中文版
热门文章
- 还在用 open 读文件?out 了,这个库比 open 好用 100 倍
- 2千万用户APP的开发运营流程
- 流量主开通以及添加广告步骤
- 从开发的角度看待产品需求评审会
- 一个计算机爱好者的不完整回忆(三十八)我的手机
- fabricjs 中自定义控件图标
- JasperReport、iReport以及JavaBean绑定表格动态赋值
- 数据链路层协议的三个基本问题
- 基于SSM框架的超市进销存管理系统
- Java异常处理:SSL证书异常:SSLHandshakeException: sun.security.validator.ValidatorException