归并排序之从微观看递归

news2024/11/18 13:33:29

前言

这次,并不是具体讨论归并排序算法,而是利用归并排序算法,探讨一下递归。归并排序的特点在于连续使用了两次递归调用,这次我们将从微观上观察递归全过程,从本质上理解递归,如果能看完,你一定能变得更强!

代码

先直接上代码吧!

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的工作:
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 = left
    j = mid+1
    for k in range(left, right+1):
        if i > mid:
            arr[k] = arr_copy[j-left]
            j = j + 1
        elif j > right:
            arr[k] = arr_copy[i-left]
            i = i + 1
        elif arr_copy[i-left] < arr_copy[j-left]:
            arr[k] = arr_copy[i-left]
            i = i + 1
        else:
            arr[k] = arr_copy[j-left]
            j = j + 1


def __merge_sort(arr, left, right):
    if left >= right:
        return
    mid = (left + right) // 2
    print(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)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/920109.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LLMs领域适应的预训练Pre-training for domain adaptation

到目前为止&#xff0c;我强调了在开发应用程序时&#xff0c;您通常会使用现有的LLM。这为您节省了大量时间&#xff0c;并可以更快地得到一个工作原型。 但是&#xff0c;有一种情况下&#xff0c;您可能会发现有必要从头开始预训练自己的模型。如果您的目标领域使用的词汇和…

无涯教程-PHP - Session选项

从PHP7 起&#xff0c; session_start()()函数接受一系列选项&#xff0c;以覆盖在 php.ini 中设置的会话配置指令。这些选项支持 session.lazy_write &#xff0c;默认情况下此函数为on&#xff0c;如果会话数据已更改&#xff0c;则会导致PHP覆盖任何会话文件。 添加的另一个…

四、MySQL性能优化

1、SQL性能优化 1、如何分析SQL的性能&#xff1f; 我们可以使用EXPLAIN命令来分析SQL的执行计划 &#xff0c;执行计划是指一条SQL语句在经过MySQL查询优化器的选择后具体的执行方式 EXPLAIN并不会真的去执行相关的语句&#xff0c;而是通过查询优化器 对语句进行分析&…

lvs实现DR模型搭建

一&#xff0c;实现DR模型搭建 1&#xff0c; 负载调度器配置 1.1调整ARP参数 vim /etc/sysctl.conf net.ipv4.conf.all.send_redirects 0 net.ipv4.conf.default.send_redirects0 net.ipv4.conf.ens33.send_redirects 0 sysctl -p 1.2 配置虚拟IP地…

Android Jetpack Compose中使用字段验证的方法

Android Jetpack Compose中使用字段验证的方法 数据验证是创建健壮且用户友好的Android应用程序的关键部分。随着现代UI工具包Jetpack Compose的引入&#xff0c;处理字段验证变得更加高效和直观。在这篇文章中&#xff0c;我们将探讨如何在Android应用中使用Jetpack Compose进…

AI开源大模型的特点和优势

目前有许多开源的大型AI模型&#xff0c;这些模型在自然语言处理、图像识别、语音处理等领域取得了显著的成果。以下是一些常见的开源大模型及其特点&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1…

matlab使用教程(22)—非线性优化函数的设置

1.设置优化选项 可以使用由 optimset 函数创建的 options 结构体来指定优化参数。然后&#xff0c;可以将 options 作为输入传递给优化函数&#xff0c;例如&#xff0c;通过使用以下语法调用 fminbnd x fminbnd(fun,x1,x2,options) 或使用以下语法调用 fminsearch x f…

使用antd mobile的PickerView二次封装成业务所需选择器

这里写目录标题 前言组件选择器视图选择器组件使用示例 注意事项扩展 前言 基于 Vant的checkbox配合popup和input定制选择器 实现一个React的版本 组件库&#xff1a;antd mobile 适用&#xff1a;移动端 UI如下&#xff1a; 功能点&#xff1a; 选择器在弹出层内&#xff0…

四、pikachu之文件包含

文章目录 1、文件包含漏洞概述1.1 文件包含漏洞1.2 相关函数1.3 文件包含漏洞分类 2、File Inclusion(local)3、File Inclusion(remote) 1、文件包含漏洞概述 1.1 文件包含漏洞 文件包含漏洞&#xff1a;在web后台开发中&#xff0c;程序员往往为了提高效率以及让代码看起来更…

AutoSAR 简介

文章目录 什么是 AutoSAR分类AutoSAR 的作用AutoSAR 架构术语介绍工具链介绍参考 随着汽车行业的发展和对汽车电子软件需求的增加&#xff0c;AutoSAR 将会变得越来越重要&#xff0c;但 AutoSAR 入门门槛高、工具链价格昂贵&#xff0c;动辄几百上千万的软件使用授权费对 OEM、…

三种主要的云交付服务和安全模型

对于许多企业来说&#xff0c;当今的数字化转型之旅包括一个关键决策&#xff1a;采用符合其需求的云交付服务。 云计算已成为现代 IT 基础设施的主要组成部分&#xff0c;具有从可扩展性到成本效率等诸多优势。然而&#xff0c;与所有技术一样&#xff0c;云也有其自身的网络…

Linux 定时任务 crontab 用法学习整理

一、linux版本 lsb_release -a 二、crontab 用法学习 2.1&#xff0c;crontab 简介 linux中crontab命令用于设置周期性被执行的指令&#xff0c;该命令从标准输入设备读取指令&#xff0c;并将其存放于“crontab”文件中&#xff0c;以供之后读取和执行。cron 系统调度进程。…

Nets3e v1.1.4(攻击者在受害者主机上偷拍并弹出受害者个人照片)

Github>https://github.com/MartinxMax/Nets3e/tree/Nets3e_V1.1.4 首页 历史更新: Nets3e v1.1.4 新增echo参数,-g -echo,生成payload后,受害者泄露的个人照片将会在受害者的主机上弹出展示 Nets3e v1.1.3 修复受害者无法获取公网IP,新增钉钉实时监控推送 Nets3e v1.1…

73 # 发布自己的 http-server 到 npm

1、添加 .npmignore 文件&#xff0c;忽略不需要的文件 public2、去官网https://www.npmjs.com/检查自己的包名是否被占用 3、切换到官方源&#xff0c;然后检查确认 nrm use npm nrm ls4、登录 npm 账号 npm login5、发布 npm publish6、查看发布情况&#xff0c;发布成功…

虚拟机中Ubuntu 16.04 设置网络

1、打开虚拟机的“编辑”选项&#xff0c;选择“虚拟网络编辑器”&#xff0c;修改网络配置 2、同时打开 Windows下面的命令行&#xff0c;输入ipconfig&#xff0c;看到虚拟网络适配器的地址 1-虚拟机-设置-网络适配器 2-编辑-虚拟网络编辑器-VMnet0 3-编辑-虚拟网络编辑…

Web安全测试(二):HTTP状态码、响应和url详解

一、前言 结合内部资料&#xff0c;与安全渗透部门同事合力整理的安全测试相关资料教程&#xff0c;全方位涵盖电商、支付、金融、网络、数据库等领域的安全测试&#xff0c;覆盖Web、APP、中间件、内外网、Linux、Windows多个平台。学完后一定能成为安全大佬&#xff01; 全部…

重新认识Android中的线程

线程的几种创建方式 new Thread&#xff1a;可复写Thread#run方法。也可以传递Runnable对象&#xff0c;更加灵活。缺点&#xff1a;缺乏统一管理&#xff0c;可能无限制新建线程&#xff0c;相互之间竞争&#xff0c;及可能占用过多系统的资源导致死机或oom。 new Thread(new…

【网络安全知识】Windows系统安全加固安全加固分析、概念及账户管理和认证权限

Windows系统安全加固分析 最小化方式安装 为了提高系统的安全性&#xff0c;采用最小化方式安装是最可靠的&#xff0c;只安装网络 服务所必需的组件。如果以后有新的服务需求&#xff0c;再安装相应的服务组件 &#xff0c;并及时进行安全设置。 系统加固工作 对Windows系统安…

GCC编译器优化等级的选择

GCC编译器通常提供多个优化级别供选择&#xff0c;每个级别对代码进行不同程度的优化。以下是GCC编译器常见的优化级别&#xff1a; 无优化&#xff08;-O0&#xff09;&#xff1a;这是默认的优化级别&#xff0c;编译器不执行任何优化。适用于调试目的或要求最小编译时间的情…

windows系统下安装Nodejs并安装vue-cli

windows下安装nodejs 下载安装淘宝镜像加速器(cnpm)安装vue-cli初始化一个vue-cli程序 下载 Node.js下载地址 根据自己的电脑选择下载 下载完成后点击下载好的文件 一路next 安装地址可以根据自己的喜好修改 按住win r 输入cmd&#xff0c;按回车&#xff0c;打开命令窗口…