目录

  • 十大经典排序算法江山图

  • 归并排序

    • 算法描述

    • 算法思想

    • 动图演示

    • 代码实现

    • 稳定性分析

    • 时间复杂度分析

    • 空间复杂度分析

  • 归并排序和快速排序对比

十大经典排序算法江山图

十大经典排序算法江山图

冒泡,选择和插入排序,它们的时间复杂度都是O(n2),比较高,适合小规模数据的排序;希尔排序和快速排序都不稳定,这篇我们来说说稳定的归并排序。归并排序在数据量大且数据递增或递减连续性好的情况下,效率比较高,且是O(nlogn)复杂度下唯一一个稳定的排序,致命缺点就是空间复杂度O(n)比较高。

实际上,工程里面的算法都是结合这些算法的优缺点来使用的,例如Java1.8源码中Arrays.sort()排序函数,就同时使用了插入排序,快速排序和归并排序:

  1. 在元素小于 47 的时候用插入排序;

  2. 大于 47 小于 286 用双轴快排;

  3. 大于 286 用 timsort 归并排序,并在 timsort 中记录数据的连续的有序段的的位 置,若有序段太多,也就是说数据近乎乱序,则用双轴快排;

  4. 上面提到的快排的递归调用的过程中,若排序的子数组数据数量小,用插入排序。

归并排序使用的就是分治思想,这个思想我在上一篇讲希尔排序的时候也提到过,归并排序是高效算法设计中最典型的一个了。分治,顾名思义,就是分而治之,将一个大问题分解成小的子问题来解决,小的子问题解决了,大问题也就解决了。分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧,这两者并不冲突。

道理咱都懂,怎么分,怎么治才是硬伤!!!

归并排序

归并排序有两种,自顶向下和自底向。

算法描述

先拆分再归并,将一个大的无序数组,拆分成两个,先处理左边再处理右边(可以对比二叉树前序遍历),一直递归拆分直至只有一个元素然后两两进行归并,一直重复这个过程直至合并完所有的子数组得到有序的完整数组,过程如下图。

自顶向下归并:

自顶向下归并

自底向上归并:

上面过程的逆过程,如图

自底向上归并

算法思想

分治,分而治之,将原数组一直拆分成左右两个小数组,直至小数组元素个数为1,然后每两个小数组进行有序归并,直至归并成一个完整的数组,从而达到整个数组有序的目的。由于每两个小的数组都是有序的,所以在每次合并的时候是很快的。

拆分都是无脑二分,直接用个递归拆分就可以,灵魂是归并过程,下面来图解下归并过程:

由图中黄色部分可知,实现归并时将两个不同的有序数组归并到第三个数组中了,这里借助了第三个数组。但是,我们需要进行很多次归并,这样每次归并时都创建一个新数组来存储排序结果就会浪费空间,因此我们可以只创建一个和原数组同样大小的数组作为辅助空间。但是这个数组不是用于存放归并后的结果,而是存放归并前的结果,然后将归并后的结果一个个从小到大放入原来的数组中,可知归并排序还需要一个n大小空间内存进行辅助排序,空间复杂度O(n)。如果原数组很大那么需要双倍的内存空间来排序,这里就是我上面提到的归并排序的最大的弊端,因为上几篇提到的排序那都是在原数组上面进行操作就可以的呢。

准备

归并1

归并2

归并3,边界处理

动图演示

归并排序

代码实现

如果上面的拆分和归并都理解了的话,写代码应该不难,静下心来写即可,看代码的理解的时候,如果对递归过程不好理解,可以在idea里面对代码进行打断点看全部的过程。

自顶向下,使用递归:

public class MergeSort {public static int[] mergeSort(int[] arr) {int[] arr_temp = Arrays.copyOf(arr, arr.length);sort(arr, arr_temp, 0, arr.length-1);return arr;}public static void sort(int[] arr, int[] arr_temp, int left, int right) {// 递归终止条件,子数组长度为1if (left >= right) {return;}int mid = (left + right) / 2;sort(arr, arr_temp, left, mid);sort(arr, arr_temp, mid + 1, right);merge(arr, arr_temp, left, mid, right);}public static void merge(int[] arr, int[] arr_temp, int left, int mid, int right) {System.arraycopy(arr, left, arr_temp, left, right - left + 1);int i = left;int j = mid + 1;// 将需要合并的两个数组合起来全部遍历一遍,将其放入临时数组for (int k = left; k <= right; k++) {// 先考虑两个边界问题,左边指针和右边指针都到头了,即一边处理结束的情况// 左半边元素全部处理完毕,右半边元素都大于左半边,右边元素直接落下来到临时数组,右边指针动if (i > mid) {arr[k] = arr_temp[j];j ++;// 右半边指针到头了,左半边元素都大于右半边,左边元素直接落下来到临时数组,左边指针动} else if (j > right) {arr[k] = arr_temp[i];i ++;// 比较,遍历到的左边元素小于右边元素,左边元素进入临时数组,左边指针右动一位} else if (arr_temp[i] < arr_temp[j]) {arr[k] = arr_temp[i];i ++;// 比较,遍历到的左边元素大于右边元素,右边元素进入临时数组,右边指针向右动一位} else {arr[k] = arr_temp[j];j ++;}}}public static void main(String[] args) {//int[] arr = {5, 7, 8, 3, 1, 2, 4, 6, 8};int[] arr = {3, 1, 2, 4};//int[] arr = {1, 2, 3};arr = mergeSort(arr);for (int i=0; i<arr.length; i++) {System.out.println(arr[i]);}}
}

自底向上,非递归的循环方式:

public class MergeSort {// 非递归式的归并排序public static int[] mergeSort(int[] arr) {int n = arr.length;// 子数组的大小分别为1,2,4,8...// 刚开始合并的数组大小是1,接着是2,接着4....for (int i = 1; i < n; i += i) {//进行数组进行划分int left = 0;int mid = left + i - 1;int right = mid + i;//进行合并,对数组大小为 i 的数组进行两两合并while (right < n) {// 合并函数和递归式的合并函数一样merge(arr, left, mid, right);left = right + 1;mid = left + i - 1;right = mid + i;}// 还有一些被遗漏的数组没合并,千万别忘了// 因为不可能每个字数组的大小都刚好为 iif (left < n && mid < n) {merge(arr, left, mid, n - 1);}}return arr;}// 合并函数,把两个有序的数组合并起来// arr[left..mif]表示一个数组,arr[mid+1 .. right]表示一个数组private static void merge(int[] arr, int left, int mid, int right) {//先用一个临时数组把他们合并汇总起来int[] a = new int[right - left + 1];int i = left;int j = mid + 1;int k = 0;while (i <= mid && j <= right) {if (arr[i] < arr[j]) {a[k++] = arr[i++];} else {a[k++] = arr[j++];}}while(i <= mid) a[k++] = arr[i++];while(j <= right) a[k++] = arr[j++];// 把临时数组复制到原数组for (i = 0; i < k; i++) {arr[left++] = a[i];}}
}

稳定性分析

稳定。

归并排序稳不稳定关键要看merge()函数,也就是两个有序子数组合并成一个有序数组的那部分代码。在合并的过程中,如果有相同的元素,可以按照顺序依次放入原数组中,这样就保证了值相同的元素,在合并前后的先后顺序不变。所以,归并排序是一个稳定的排序算法。

时间复杂度分析

归并排序涉及递归,时间复杂度的分析稍微有点复杂。我们正好借此机会来学习一下,如何分析递归代码的时间复杂度。

递归的适用场景是,一个问题a可以分解为多个子问题b、c,那求解问题a就可以分解为求解问题b、c。问题b、c解决之后,我们再把b、c的结果合并成a的结果。

如果我们定义求解问题a的时间是T(a),求解问题b、c的时间分别是T(b)和 T( c),那我们就可以得到这样的递推关系式: T(a) = T(b) + T(c) + K,其中K等于将两个子问题b、c的结果合并成问题 a 的结果所消耗的时间。

套用这个公式,我们来分析一下归并排序的时间复杂度。我们假设对n个元素进行归并排序需要的时间是T(n),那分解成两个子数组排序的时间都是T(n/2)。我们知道,merge()函数合并两个有序子数组的时间复杂度是O(n)。所以,套用前面的公式,归并排序的时间复杂度的计算公式就是:

T(1) = C; n=1时,只需要常量级的执行时间,所以表示为C。

T(n) = 2*T(n/2) + n; n>1

通过这个公式,如何来求解T(n)呢?还不够直观?那我们再进一步分解一下计算过程。

T(n) = 2*T(n/2) + n= 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n= 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n= 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n ......= 2^k * T(n/2^k) + k * n

通过这样一步一步分解推导,我们可以得到T(n) = 2^kT(n/2^k)+kn。

当T(n/2^k)=T(1)时,也就是n/2^k=1,我们得到 k=log2n 。

我们将k值代入上面的公式,得到T(n)=Cn+nlog2n 。如果我们用大O标记法来表示的话,T(n)就等于O(nlogn)。所以归并排序的时间复杂度是O(nlogn)。

从我们的原理分析可知,归并排序的执行效率与要排序的原始数组的有序程度无关,所以其时间复杂度是非常稳定的,不管是最好情况、最坏情 况,还是平均情况,时间复杂度都是O(nlogn)。

空间复杂度分析

归并排序的时间复杂度任何情况下都是O(nlogn),看起来非常优秀,因为上一篇分析到的即便是快速排序,最坏情况下,时间复杂度也是O(n2)。

注:快速排序算法虽然最坏情况下的时间复杂度是O(n2),但是平均情况下时间复杂度都是O(nlogn)。不仅如此,快速排序算法时间复杂度退化到O(n2)的概率非常小, 我们可以通过合理地选择pivot来避免这种情况。。

但是,归并排序并没有像快排那样,应用广泛,因为它有一个致命的“弱点”,那就是归并排序不是原地排序算法。

通过代码和上面的讲解也知道我借助了和原数组大小相同的数组来进行辅助排序,所以空间复杂度是O(n)。

归并排序和快速排序对比

快排不理解的,看我上一篇 极客算法训练笔记(六),十大经典排序之希尔排序,快速排序

相同点:

  1. 都采用分治算法

  2. 都可以递归实现

  3. 平均时间复杂度都是O(nlogn)

不同点:

  1. 归并排序是先切分、后排序,快速排序是切分、排序交替进行;

  2. 归并排序是稳定的排序,而快速排序是不稳定的排序;

  3. 归并排序在最坏和最好情况下的时间复杂度均为O(nlogn),而快速排序最坏O(n^2),最好O(n);

  4. 快速排序是原地排序,归并不是。

下一篇是堆排序,有收获的朋友帮忙点个赞吧,感谢,另外欢迎关注公众号《阿甘的码路》,各大平台号也有内容同步只不过排版可能会乱~

参考资料:极客时间算法训练营笔记,数据结构与算法之美,算法书籍

极客算法训练笔记(七),十大经典排序之归并排序,全网最详相关推荐

  1. 极客算法训练笔记(六),十大经典排序之希尔排序,快速排序

    目录 抛砖引玉 希尔排序 快速排序 抛砖引玉 十大经典排序算法江山图 十大经典排序算法江山图 排序算法的衡量指标我这里不再重复,上一篇我已经列举分析的很清楚了,但是非常重要,没看到我上一篇的小伙伴墙裂 ...

  2. 可由一个尾指针唯一确定的链表有_极客算法训练笔记(三),链表详细图解,别再逃避了朋友...

    目录 缓存引爆链表 链表单链表双向链表循环链表双向循环链表 LinkedHashMap实现LRU缓存,源码解析(JDK1.8) 算法 爬楼梯 算法 反转链表 算法 链表环检测 缓存引爆链表 存储结构 ...

  3. 严蔚敏算法约瑟夫环_极客算法训练笔记(三),链表详细图解,别再逃避了朋友...

    目录 缓存引爆链表 链表 单链表 双向链表 循环链表 双向循环链表 LinkedHashMap实现LRU缓存,源码解析(JDK1.8) 算法 爬楼梯 算法 反转链表 算法 链表环检测 缓存引爆链表 存 ...

  4. 算法设计与分析——十大经典排序算法一(1--5)

    目录 算法设计与分析--十大经典排序算法 第1关:冒泡排序 参考代码 第2关:选择排序 参考代码 第3关:插入排序 参考代码 第4关:希尔排序 参考代码 第5关:归并排序 参考代码 作者有言 一个不知 ...

  5. 算法设计与分析——十大经典排序算法二(6--10)

    一个不知名大学生,江湖人称菜狗 original author: jacky Li Email : 3435673055@qq.com  Time of completion:2023.3.1 Las ...

  6. java array 元素的位置_数据结构与算法:动态图解十大经典排序算法(含JAVA代码实现)...

    点击上方"JAVA",星标公众号 重磅干货,第一时间送达 本文将采取动态图+文字描述+正确的java代码实现来讲解以下十大排序算法: 冒泡排序 选择排序 插入排序 希尔排序 归并排 ...

  7. 一文搞掂十大经典排序算法

    一文搞掂十大经典排序算法 今天整理一下十大经典排序算法. 1.冒泡排序 --越小的元素会经由交换慢慢"浮"到数列的顶端 算法演示 算法步骤 比较相邻的元素.如果第一个比第二个大,就 ...

  8. 十大经典排序算法详解(一)冒泡排序,选择排序,插入排序

    养成习惯,先赞后看!!! 你的点赞与关注真的对我非常有帮助.如果可以的话,动动手指,一键三连吧!!! 目录 1.算法的评判标准 2.排序算法的分类 3.十大经典排序算法-冒泡排序,选择排序,插入排序 ...

  9. 数据结构与算法笔记 —— 十大经典排序及算法的稳定性

    一.十大经典排序算法 排序算法是<数据结构与算法>中最基本的算法之一. 排序算法可以分为内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全 ...

  10. 十大经典排序算法动画与解析,看我就够了

    作者 | 程序员小吴 转载自五分钟学算法(ID: CXYxiaowu) 排序算法是<数据结构与算法>中最基本的算法之一. 排序算法可以分为内部排序和外部排序.内部排序是数据记录在内存中进行 ...

最新文章

  1. 关于如何准备一份制胜简历的清单
  2. Windows-Server下加强系统安全性系列之方案【八】
  3. 【渝粤题库】广东开放大学 标准法律法规 形成性考核
  4. typecho模板ajax,typecho ajax登陆
  5. 华为发布近2万元折叠屏手机Mate Xs;iPhone 12或支持WiFi新标;Electron 6.1.8发布 | 极客头条...
  6. C++之move提升copy性能
  7. kafka的docker文件
  8. 我关注的那些程序员大佬
  9. matlab傅里叶光学仿真,计算傅里叶光学实现衍射图样的计算
  10. hanlp 如何快速从分词仅取出人名
  11. css中调整高度充满_css实现div的高度填满剩余空间
  12. 银广夏事件--中国股票财务作假事件
  13. 十二黄金圣斗士阴险程度(爆笑)
  14. VS2019+WDK10编写xp平台的驱动
  15. IOS 苹果自带地图、百度地图、高德地图打开方式
  16. 【搬运工】值得一提的生活窍门
  17. ios用 shell 自动打包静态库(里面包含真机和模拟器)
  18. oracle数据库问题吗,Oracle数据库常见问题答疑一
  19. 【转】知识图谱上推荐推理的模仿学习框架
  20. 叉包舍 - PPPoE之常见拨号错误代码及处理

热门文章

  1. Esp8266 -- 心知天气get请求及url讲解说明
  2. Amazon,我们完全不能接受 — 因此我们必须变更 Elastic 许可协议
  3. 西门子组态软件wincc短信报警,微信推送
  4. 中国工业内窥镜市场全景调研与投资前景预测报告2022-2028年
  5. 串口通信的隔离传输方案记录
  6. 洛谷P2000 拯救世界(NTT+生成函数)
  7. 物联网发展跨越拐点!2020 AIoT产业年终盛典圆满落幕
  8. ubuntu下deactivate matlab的操作
  9. 深入研究webrtc平滑发送(paced sender)
  10. 【python初级】 关于time.sleep睡眠时间