归并排序之从微观看递归
前言
这次,并不是具体讨论归并排序算法,而是利用归并排序算法,探讨一下递归。归并排序的特点在于连续使用了两次递归调用,这次我们将从微观上观察递归全过程,从本质上理解递归,如果能看完,你一定能变得更强!
代码
先直接上代码吧!
using System.CodeDom.Compiler;int _1 = 0;
int _2 = 0;void __merge(int[] arr, int left, int mid, int right, string flag)
{ Console.WriteLine($"__merge_{flag}: left={left+1}, mid={mid + 1}, right={right + 1}");int[] copy = new int[right - left + 1];//copy arr[left,right] to copy[]for (int ii = left; ii <= right; ii++){copy[ii - left] = arr[ii];}int i = left;int j = mid + 1;for (int k = left; k <= right; k++){if (i > mid){arr[k] = copy[j-left];j++;}else if (j > right){arr[k] = copy[i - left];i++;}else if (copy[i - left] < copy[j - left]){arr[k] = copy[i - left];i++;}else{arr[k] = copy[j - left];j++;}}
}void __merge_sort(int[] arr, int left, int right, string flag)
{if (left >= right)return;if (flag.Contains("1")){_1 += 1;}if (flag.Contains("2")){_2 += 1;}int mid = (left + right) / 2;Console.WriteLine($"{flag}, left={left+1}, mid={mid+1}, right={right + 1}");__merge_sort(arr, left, mid, "第1个merge_sort");__merge_sort(arr, mid + 1, right, "第2个merge_sort");__merge(arr, left, mid, right, flag);
}void merge_sort(int[] arr)
{__merge_sort(arr, 0, arr.Length - 1, "第0个merge_sort");
}int[] arr = { 1, 3, 5, 7, 8, 2, 4, 6};
merge_sort(arr);Console.WriteLine($"_1:{_1}||_2:{_2}");
foreach (var item in arr)
{Console.Write(item + " ");
}Console.ReadLine();
递归分析
这段代码,特殊的地方在于,它使用了两次递归:
_1 和 _2 记录了 第一个和第二个递归的调用次数(和算法逻辑无关),这里增加的flag参数也主要是为了分析递归的过程。
第一个 __merge_sort 递归 的作用主要是将左边的一个数组不断的进行二分。
第二个 __merge_sort 递归 的作用主要是将右边的一个数组不断的进行二分。
merge将二分的数组按照大小顺序合二为一!
这个算法实现的难度,在于递归的构造和数组边界的把握。
宏观上看
void __merge_sort(int[] arr, int left, int right)
{int mid = (left + right) / 2;__merge_sort(arr, left, mid);__merge_sort(arr, mid + 1, right);__merge(arr, left, mid, right, flag);
}
过程就是,通过__merge_sort的递归,将数组二分,然后再将二分的数组归并。
__merge进行归并的前提是,两个即将归并的数组为已经排好序的数组!
但是,如果我们二分的到单个数字的时候,一个数字就是一个数组,这个数字也可以看成是
有序的数组。
所以,当二分到”极致的“时候,就满足了__merge的前提。
二分完成之后,以下就Merge的工作:
看到这张图,其实很容易联想到递归算法,但是如何构造递归函数呢?有点像:
要把大象装冰箱总共分几步?这是宏观上的看到的:
1 第一步分左边: __merge_sort(arr, left, mid);
2 第二步分右边: __merge_sort(arr, mid + 1, right);
3 第三步整合到一起: __merge(arr, left, mid, right, flag);
微观上看
我们先从微观上从本质上,看看整个递归过程是这么执行的(请结合下面两张图观看):
这个是程序的执行结果,第0个 表示最外层的__merge_sort被调用。
此时最左边的是1,中间为4,最右是8.
然后__merge_sort一个递归调用触发,第一个__merge_sort负责左边。
所以是:最左边的是1,中间为2,最右是4. 此时并没有满足递归退出的条件,
所以继续调用第一个__merge_sort。此时继续负责左边(注意是1 2 3 4 的左边)。
所以就有了1 1 2 ,那么很明显下次递归的时候,左边会等于右边(left >= right),所以下次就会满足递归退出的条件。
下面一段是重点:
所以下一次,开始了第二个递归的调用!他负责右边的二分。这里可能会有人觉得奇怪,不是负责右边的调用吗?怎么打印的是3 3 4 ?这是左边啊!
那我是这么理解的,递归是有层级划分的,每递归一层就像下了一层楼梯 , 每次递归返回,就是上了一层台阶 刚刚我们退出时候,其实是处于二分 1 2 3 4 这层阶梯的,所以此时,在整个层级,需要二分的是 1 2 3 4 的右边!所以二分的是3 3 4。
此时,该层的__merge_sort也要返回到上一层了。
此时打印的是 5 6 8,直接分的就是 右边的 5 6 7 8,这是因为上一层的左边的 1 2 3 4 已经在上一次的递归中已经被分过了!(递归每一层都有自己的记忆,其实就是每一层的参数都压到栈里进行的保存)此时已经到了递归的最上层了,而且第一层的左右两边都分完了。
接下来开始,是继续往下一层递归,左边的1 2 3 4 已经二分完毕,所以是右边的 5 6 7 8,
而 5 6 7 8 也已经被 分成了 56 | 78。 所以,又是 第一个 __merge_sort 开始二分左边的 56了。
所以此时打印的是 5 5 6,最后是 第二个将右边的分为 7 7 8. 整个二分的过程就结束了。
要注意的是,两个__merge_sort始终是处于用一个层级的,当第一个__merge_sort下个几个楼梯后,其实第二个也会下同样多个阶梯。(接下来还会进一步的再次说明这一点)
合并的部分
接下来,我们来单独看看,二分之后 __merge这个函数的调用过程:
这个完全是符合预期的:
显示左边的,先合并12,再合并14,接着合并1234
然后是右边的,先合并56,再嗯好吧78,结果合并5678
最后是 148,也就是 12345678整个的合并!
现在,我们结合递归和合并一起看,是怎么样的一个顺序:
代码回顾
int mid = (left + right) / 2;Console.WriteLine($"{flag}, left={left+1}, mid={mid+1}, right={right + 1}");__merge_sort(arr, left, mid);__merge_sort(arr, mid + 1, right);__merge(arr, left, mid, right, flag); } ```
首先是,第一次__merge_sort 三次连续的递归之后,直接就开始了第一次的合并!
这里,可能有人会问:按照函数的调用顺序,此时不应该执行,第二个__merge_sort吗?这么直接调到了
__merge函数了?第二个__merge_sort不会执行吗?
这里,我再次强调层级的问题,现在已经递归到最后一个层级了,此时left mid right
对应的是 1 1 2,其实就是对 12 进行二分,此时 对应在这个层级的第二个__merge_sort来说:
__merge_sort(arr, mid + 1, right);
left = mid+1 所以此时,满足了递归的退出条件 left >= right,(其实就是只剩下2了不用你右边在分了!)
所以此时不是第二个__merge_sort没有调用,而是直接退出了。(递归的退出条件也是递归的最重要的核心之一)
所以就执行的__merge,完成12合并(合并的过程其实就排序,可以参考最上面的__merge代码)。
此时,递归已经触底的,开始返回到上一次,上一层的左边已经递归完成(12已经二分,也满足递归退出条件)所以上一层阶梯,就开始右边的递归,将34 二分(注意:这里124左右的划分全部结束啦),二分完成后就返回了,
于是就会执行__merge,完成 34的合并。在这次,__merge结束后,紧接着又是一个
__merge,完成 1 2 4 的合并,也就是说,前面两个__merge_sort都被跳过了!
这是为啥?
这是因为__merge执行完后,此时递归又会上一个层级,在这个层级,其实就是1 2 4的二分,
而 1 2 4 左和右的划分在之前的递归过程中已经结束了,所以直接开始合并了。
此时,还剩下的部分是:
合并完成之后,这一次递归也返回了,就到了最上面一层递归了,不过左边的部分已经执行过了,所以是,右边的 5 6 8 的 划分,划分玩之后,从第二个__merge_sort,再次进入递归(下一层楼梯)此时遇到了下一层的第一个__merge_sort。于是就有了 5 5 6,已经触底了所以返回遇到了这一层的第二个__merge_sort就有了 778。到此两个递归都已经触底且都已完成,接下来就都是merge合并了!
这里说一些感想,读到这里你应该体会到了调用两个递归的特点,一开始遇到第一个递归,就会一直递归到最下面一层,然后一层层返回,如下:
在返回的过程中会调用 倒数第二层的第二个__merge_sort, 所以第一个__merge_sort,在递归下楼梯的时候调用,而第二个递归是在上楼梯的时候调用,而当上到最上层的时,刚刚调用完了第二个__merge_sort,又会进入递归的下一层,并碰再次遇到第一个__merge_sort,并再次进入第一层递归!再次触底!
次数问题
接下来再看另外一个问题(和递归无关)如果把数组扩大到10:
这次,负责左边的递归运行了5次,而负责右边的只运行了3次。这次左右不平衡了?
会觉得奇怪吗?
这是因为奇偶数的问题,当 数组为8的时候, 8 二分 后是 4+ 4,最后变成 2+2+2+2。
在变成单个之前都是偶数。如果是10,二分就会变成5。5这个数字就会导致二分时,左边的二分次数会更多。
所以只有当个数为 2的N次方的时候,比如 8 16,这样的数组长度时,两次递归的调用次数才会相同!
递归小结
看到,最后你还能回忆起,__merge_sort是如何实现二分的吗?
想不起来,没关系,因为这个过程很隐秘,不过也是递归的设计的关键所在。
void __merge_sort(int[] arr, int left, int right)
{int mid = (left + right) / 2;__merge_sort(arr, left, mid);__merge_sort(arr, mid + 1, right);__merge(arr, left, mid, right, flag);
}
首先,我们要自己设计递归函数,比如传入一个数组,我们的目的是改变该数组内部的元素的顺序,但是,每次考虑的是其中的一个部分。所以我需要一个边界,left和right。
对于整个数组来说,left是0,right是长度-1;
二分之后,每次二分之后,left和right都会发生变化。
每次递归调用都会下一层阶梯,进入下一层,从而导致left和right的再次改变。
能理解 ”进入下一层“ 是理解递归的关键,在一次次递归中,就完成了二分的过程!
我们,可先从宏观上设计思路,再从微观上确保思路的正确。
这篇文章,写了很久,自我感觉良好,不知道各位觉得如何,欢迎评论区反馈~~~
附加,在提供一下完整的python代码吧
之前本来是用python测试,不过还是觉得vs调试C#方便啊:
def __merge(arr, left, mid, right):arr_copy = arr[left:right + 1][:]i = leftj = mid+1for k in range(left, right+1):if i > mid:arr[k] = arr_copy[j-left]j = j + 1elif j > right:arr[k] = arr_copy[i-left]i = i + 1elif arr_copy[i-left] < arr_copy[j-left]:arr[k] = arr_copy[i-left]i = i + 1else:arr[k] = arr_copy[j-left]j = j + 1def __merge_sort(arr, left, right):if left >= right:returnmid = (left + right) // 2print(left, mid, right)__merge_sort(arr, left, mid)__merge_sort(arr, mid + 1, right)__merge(arr, left, mid, right)def merge_sort(arr):__merge_sort(arr, 0, len(arr) - 1)if __name__ == '__main__':arr0 = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10]merge_sort(arr0)print(arr0)
相关文章:
归并排序之从微观看递归
前言 这次,并不是具体讨论归并排序算法,而是利用归并排序算法,探讨一下递归。归并排序的特点在于连续使用了两次递归调用,这次我们将从微观上观察递归全过程,从本质上理解递归,如果能看完,你一…...
Pytorch-day07-模型保存与读取
PyTorch 模型保存&读取 模型存储模型单卡存储&多卡存储模型单卡读取&多卡读取 1、模型存储 PyTorch存储模型主要采用pkl,pt,pth三种格式,就使用层面来说没有区别PyTorch模型主要包含两个部分:模型结构和权重。其中模型是继承n…...
【C语言每日一题】01. Hello, World!
题目来源:http://noi.openjudge.cn/ch0101/01/ 01. Hello, World! 总时间限制: 1000ms 内存限制: 65536kB 问题描述 对于大部分编程语言来说,编写一个能够输出“Hello, World!”的程序往往是最基本、最简单的。因此,这个程序常常作为一个初…...
arm: day8
1.中断实验:按键控制led灯 流程: key.h /*************************************************************************> File Name: include/key.h> Created Time: 2023年08月21日 星期一 17时03分20秒***************************************…...
k8s容器加入host解析字段
一、通过edit或path来修改 kubectl edit deploy /xxxxx. x-n cattle-system xxxxx为你的资源对象名称 二、添加字段 三、code hostAliases:- hostnames:- www.rancher.localip: 10.10.2.180...
浅谈开发过程中完善的注释的重要性
第一部分:引言 1.1 简述编程注释的定义和功能 编程注释是一种在源代码中添加的辅助性文字,它不参与编译或执行,但对于理解源代码起着至关重要的作用。注释可以简单地描述代码的功能,也可以详细地解释算法的工作原理、设计决策的…...
Docker 微服务实战
1. 通过IDEA新建一个普通微服务模块 1.1 建Module docker_boot 1.2 改写pom <?xml version"1.0" encoding"UTF-8"?><project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance&…...
JupyterHub实战应用
一、JupyerHub jupyter notebook 是一个非常有用的工具,我们可以在浏览器中任意编辑调试我们的python代码,并且支持markdown 语法,可以说是科研利器。但是这种情况适合个人使用,也就是jupyter notebook以我们自己的主机作为服务器…...
【MySQL】视图
目录 一、什么是视图 二、视图的操作 2.1 创建视图 2.2 删除视图 三、视图规则和限制 一、什么是视图 视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列带有名称的列和行数据。视图的数据变化会影响到基表(创建视图所…...
基于 Android 剧院购票APP的开发与设计
摘要:近年来,随着社会的发展和科技方面的创新,越来越多的人选择使用手机应用程序来购买剧场票。本文将探讨基于 Android 平台的剧院购票应用程序的开发和设计。该应用程序将为用户提供浏览剧场列表、查看剧场详情、选择座位并购买剧场票的功能…...
反转链表II
江湖一笑浪滔滔,红尘尽忘了 题目 示例 思路 链表这部分的题,不少都离不开单链表的反转,参考:反转一个单链表 这道题加上哨兵位的话会简单很多,如果不加的话,还需要分情况一下,像是从头节点开始…...
HTML 和 CSS 来实现毛玻璃效果(Glassmorphism)
毛玻璃效果简介 它的主要特征就是半透明的背景,以及阴影和边框。 同时还要为背景加上模糊效果,使得背景之后的元素根据自身内容产生漂亮的“变形”效果,示例: 代码实现 首先,创建一个 HTML 文件,写入如下…...
【技术】国标GB28181视频平台EasyGBS通过对应密钥上传到其他平台展示的详细步骤
国标GB28181协议视频平台EasyGBS是基于国标GB28181协议的视频云服务平台,支持多路设备同时接入,并对多平台、多终端分发出RTSP、RTMP、FLV、HLS、WebRTC等格式的视频流。平台可提供视频监控直播、云端录像、云存储、检索回放、智能告警、语音对讲、平台级…...
SpeedBI数据可视化工具:浏览器上做分析
SpeedBI数据分析云是一种在浏览器上进行数据可视化分析的工具,它能够将数据以可视化的形式呈现出来,并支持多种数据源和图表类型。 所有操作,均在浏览器上进行 在浏览器中打开SpeedBI数据分析云官网,点击【免费使用】进入&#…...
8.21笔记
Deeplab-MSc-LargrFOC 此图除了主输出之外,还有五个支线输出,他们池化层与VGG网络不同,其中卷积核大小是3,而VGG中卷积核大小为2(这个网络一开始是基于VGG网络提出的,因为那时候提出比较早,没有…...
MyBatis-Plus中公共字段的统一处理
数据库中一些表的公共字段,例如修改时间、修改人、创建时间、创建人,我们一般都是这样来处理的: employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); employee.setCreateUser(UserHolder.get()); …...
SQL的导出与导入
1、导入 使用命令行导入 1.登录sql界面; 2.create database Demo新建一个库; 3.选中数据库use Demo;选中导入路径source D:Demo.sql; 4.查看表show tables; 2、导出 整个sql mysqldump -u username -ppassword dbname > dbname.sq…...
记录一次wordpress项目的发布过程
背景:发布一套已完成的代码到线上,有完整的代码包,sql文件,环境是linux 宝塔。无wordpress相关经验。 过程:正常的发布代码 问题1:访问自己的域名后跳转到别的域名。 解决: 修改数据表wp_optio…...
HTML详解连载(8)
HTML详解连载(8) 专栏链接 [link](http://t.csdn.cn/xF0H3)下面进行专栏介绍 开始喽浮动-产品区域布局场景 解决方法清除浮动方法一:额外标签发方法二:单伪元素法方法三:双伪元素法方法四:overflow浮动-总结…...
Linux系统之安装OneNav个人书签管理器
Linux系统之安装OneNav个人书签管理器 一、OneNav介绍1.OneNav简介2.OneNav特点 二、本地环境介绍2.1 本地环境规划2.2 本次实践介绍 三、检查本地环境3.1 检查本地操作系统版本3.2 检查系统内核版本3.3 检查本地yum仓库状态 四、安装httpd服务4.1 安装httpd4.2 启动httpd服务4…...
主程技术分享: 游戏项目帧同步,状态同步如何选
网络游戏开发项目中帧同步,状态同步如何选? 网络游戏的核心技术之一就是玩家的网络同步,主流的网络同步有”帧同步”与”状态同步”。今天我们来分析一下这两种同步模式。同时教大家如何在自己的项目中采用最合适的同步方式。接下来从以下3个方面来阐述: 对啦&…...
ChatGPT-4: 半年的深度使用思考
几个月的时间一直在使用 ChatGpt-4,以口述语音转文字的形式说一下自己的体会。 1、选择版本 大前提:我使用的都是 GPT4 的版本。也就是说至少每个月要付费20$。 因为 3.5 的版本,实际上使用体验是非常差的,主要体现在答非所问上。…...
【健康医疗】Axure用药提醒小程序原型图,健康管理用药助手原型模板
作品概况 页面数量:共 20 页 兼容软件:Axure RP 9/10,不支持低版本 应用领域:健康管理,用药助手 作品申明:页面内容仅用于功能演示,无实际功能 作品特色 本作品为「用药提醒」小程序原型图…...
ERROR in static/js/xxx.js from UglifyJs
老项目用的webpack3,打包的时候遇到**ERROR in static/js/xxx.js from UglifyJs**这个报错, UglifyJS是个包含JS解释器、代码最小化、压缩、美化的工具集,是前端开发打包的最常用工具之一,只支持ES5,不支持ES6&#x…...
阿里云ECS服务器安装PostgreSQL
1. 概述 PostgreSQL是一个功能强大的开源数据库,它支持丰富的数据类型和自定义类型,其提供了丰富的接口,可以自行扩展其功能,支持使用流行的编程语言编写自定义函数 PostgreSQL数据库有如下优势: PostgreSQL数据库时…...
【核磁共振成像】傅里叶重建
目录 一、傅里叶重建二、填零三、移相四、数据窗函数五、矩形视野六、多线圈数据重建七、图像变形校正八、缩放比例九、基线校准 长TR,长TE,是T2加权像; 短TR,短TE,是T1加权像; 长TR,短TE&#…...
Camunda 工作流节点跳转 - 多实例节点判断和跳转
在多种工作流引擎中,Camunda框架对流程的处理控制更为强大、灵活。 在应对流程节点按业务需要进行自由跨节点跳转的需求时,通过代码自由控制节点的跳转在Camunda中是支持的,并且提供了编码方法,其中多实例的处理上有一些区别要特…...
MySQL不停重启问题
MySQL不停的自动杀掉自动重启 看一下log日志 my.cnf 里配置的 log_error /var/log/mysqld.log vim /var/log/mysqld.log 报的错误只是 [ERROR] Cant start server: Bind on TCP/IP port: Address already in use [ERROR] Do you already have another mysqld server …...
ol-cesium 暴露 Cesium viewer 对象以及二三维切换、viewer 添加点功能示例
ol-cesium 暴露 Cesium viewer 对象以及二三维切换、viewer 添加点功能示例 核心代码完整代码在线示例 二三维一体化的概念一直都比较火热,虽然大多数都是狭义的概念,但是很多需求方也想要这样的功能。 Openlayers 官方出了一个二三维一体化的工具&…...
国产化-达梦数据库安装2
目录 DM8数据库下载地址 安装一路狂飙next 启动服务 随着国家政府的推广、越来越多的政府项目、在系统部署需要采购国产服务器、数据库等 DM8数据库下载地址 https://eco.dameng.com/download/ 安装一路狂飙next windos安装比较简单直接next即可 仅仅记录几个关键疑问地方k…...
如何在百度举报网站/网络营销运营公司
1.java3d开发环境搭建 jdk(java development kit)安装java3d安装IDE安装(eclipse,idea,netbeans选择一种即可) 2.java3d开发引擎 JMEArdor3DXith3D 3.java3d原理 JAVA 3D从高层次为开发者提供对三维实体的创建、操纵和着色,使开发工作变得极为简单。同时&#…...
毕业设计代做网站都有哪些/如何建网站
作者: 小马哥 rstevens (rstevens2008hotmail.com) 欢迎转载,未经允许,请勿用于商业目的1. 定义 Packet: 通过网卡收发的报文,包括链路层、网络层、传输层的协议头和携带的数据 Data Buffer:用…...
阿里云备案成功怎么建设网站/seo推广优化平台
转载于:https://www.cnblogs.com/ycxyyzw/archive/2012/07/04/2576148.html...
网站需要在公安局备案/外包网络推广公司
成为经理人的捷径 (一) 其实世界上没有一步登天的捷径,“条条大路通罗马。”选择哪条、怎么走完全是个人的意愿。要能找一条最适合自己的路,想成为高效的经理人,就要靠有效的方法。接下来我们就围绕着这个“方法”展开简单的讨论。 高的效绩是…...
化妆培训学校网站开发/seo建站优化推广
谈到企业级自助分析平台,大家自然会想到Tableau,在Garnter最新的BI平台魔力象限中,是这么描述Tableau的。 “Tableau is a Leader in this Magic Quadrant. It offers a visual-based exploration experience that enables business users to…...
长春至少有500万人感染/抖音seo供应商
问卷调查demo已上传,欢迎大家指正,欢迎大家下载:https://download.csdn.net/download/lzb348110175/11085995 如果您没积分的话,可以私信/评论,然后发给您^_^ 之前也有看过Vue,但是一直都是处于大致知道点的…...