数据结构与算法:归并排序

数据结构与算法:归并排序

    • 归并思想
    • 递归法
    • 非递归


归并思想

在讲解归并排序前,我们先看到一个问题:

对于这样两个有序的数组,如何将它们合并为一个有序的数组?
在这里插入图片描述

在此我们处理这个问题的思路就是:开辟一个新的数组,然后分别安置一个指针在左右数组,利用指针遍历数组,每次对比将比较小的那个元素插入到数组的尾部。
像这样:
请添加图片描述
那么我们要如何利用这个思想,让一个无序的数组有序?

比如这个数组:
在这里插入图片描述

我们可以这样划分数组:
请添加图片描述
将其不断往小份划分,划分到最后一段:
在这里插入图片描述
对于每一个区域,我们可以认为:左边的一个元素是一个有序数组,右边的一个元素是一个有序数组。然后在对其进行一次归并。

就像这样:
在这里插入图片描述

这样我们又得到了8组有序的数组,我们继续归并:
在这里插入图片描述

以此类推:
在这里插入图片描述
归并排序就是这样一个不断划分子区间,然后进行合并的过程。
在这里插入图片描述


递归法

看到不断划分出子区间,毫无疑问这将会是一个递归的过程,而我们将子区间划分到底,再进行处理数据,所以这是同样是一个后序遍历的过程。

在进行合并数组的时候,我们会需要开辟一个新的数组来存放临时的数据。
如果每次合并数组时都额外开辟一段空间,就有点浪费时间了,空间是可以重复利用的,所以我们一开始就要开辟一个和原数组等大的空间。后序进行合并操作都在这个拷贝数组中,当合并完成后,再把数组复制回原数组即可。

那么我们的归并排序一开始要这样写,

void _MergeSort(int* a, int begin, int end, int* tmp)
{//归并主体
}void MergeSort(int* a, int n)
{int*tmp = (int*)malloc(sizeof(int) * n);//开辟空间if (tmp == NULL){perror("malloc fali!");return;}_MergeSort(a, 0, n - 1, tmp);//归并主体free(tmp);
}

_MergeSort函数是归并排序的主体函数,它接受一个待排序的数组a、数组的起始位置begin、数组的结束位置end以及一个临时数组tmp。函数中实现了归并排序的核心部分,即将数组a中的元素从位置begin到位置end进行排序。

MergeSort函数是对_MergeSort函数的封装,它接受一个待排序的数组a和数组的长度n。函数中首先动态分配了一个大小为n的临时数组tmp,用于存放归并时的临时数据,然后调用_MergeSort函数对数组a进行归并排序。排序完成后,释放临时数组tmp的空间。

接下来我们完成归并主体的代码:

void _MergeSort(int* a, int begin, int end, int* tmp)
{if (begin >= end)return;int mid = (begin + end) / 2;_MergeSort(a, begin, mid, tmp);_MergeSort(a, mid + 1, end, tmp);//归并int begin1 = begin, end1 = mid;int begin2 = mid + 1, end2 = end;int i = begin;//使拷贝的数组与原数组的位置对应while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[i++] = a[begin1++];}else{tmp[i++] = a[begin2++];}}while (begin1 <= end1){tmp[i++] = a[begin1++];}while (begin2 <= end2){tmp[i++] = a[begin2++];}memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}

下面是代码的详细步骤解释:

  1. 首先定义了一个名为_MergeSort的函数,它接受四个参数:需要排序的数组a、排序区间的起始位置begin和结束位置end,以及一个临时数组tmp

  1. 如果begin大于等于end说明只有一个元素或者没有元素,直接返回

  1. 否则,计算出中间位置mid,将数组分为左右两个子数组。

  1. 对左右两个子数组分别调用_MergeSort函数进行递归排序,直到只剩下一个元素。

  1. 接下来进行归并操作。定义四个变量begin1end1begin2end2,分别表示左右子数组的起始和结束位置。

  1. 定义一个变量i,用于标记临时数组tmp的位置。

  1. 开始合并过程,比较左右子数组的元素大小,将较小的元素放入临时数组tmp,并递增i和对应的子数组起始位置。

  1. 如果有一边的子数组已经合并完毕(起始位置大于结束位置),则将另一边的子数组中剩余的元素依次放入临时数组tmp中。

  1. 最后,使用memcpy函数将临时数组tmp中的元素拷贝回原数组a的对应位置。

通过不断递归划分数组为更小的子数组,并借助临时数组tmp进行归并操作,最终完成整个归并排序的过程。

总过程如下:

请添加图片描述


非递归

其实归并排序也可以使用非递归的方法实现:

我们再次看到这个归并排序的递归图:
在这里插入图片描述
可以发现,由于是后序遍历,其实前面在利用递归对数组划分的过程,我们并没有对数组进行任何修改,也就是说我们可以直接把数组划分到每一组只有1个元素,来模拟前半部分的递归
像这样:

int gap = 1;
for (int i = 0; i < n; i += 2 * gap)
{//归并数组
}

在这段代码中,变量 gap 初始化为 1,表示每次归并操作对应的子数组的长度。初始时,我们假定待合并的子数组的长度为 1,来模仿前半部分递归划分出来的子数组。
i += 2 * gap意味着每次跳过两个子数组,一个gap是一个子数组的长度,2 * gap就是两个子数组的长度。这模仿的是每次合并数组时,被合并的数组区间划分。

那么我们完成了前半部分递归,直接把数组划分为了只有一个元素的小区间,那要如何模仿后半部分?
递归后半部分的工作是,将小区间归并后,形成了一个大区间,接着再把大区间归并,直到这个区间等于原数组长度。
我们想用非递归的思路来模仿后半部分,也就是要实现每次归并区间的增大
那么每次归并的区间增大多少?
因为每次合并时,是合并了左右两个长度相同的数组,所以归并出的新数组长度应该是2*gap,所以我们每一趟归并,都要把gap翻倍,来模仿区间被合并后增大的效果:

void MergeSortNonR(int* a, int n)
{int gap = 1;while (gap < n)//当gap超过n,说明数组合并完毕了{for (int i = 0; i < n; i += 2 * gap){//归并主体}gap *= 2;//归并完后,下一趟归并的区间翻倍}
}

那么每次划分出了归并的区间,又要如何划分其内部的两个子数组?
比如这样:

void MergeSortNonR(int* a, int n)
{int gap = 1;while (gap < n)//当gap超过n,说明数组合并完毕了{for (int i = 0; i < n; i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;//对[begin1, end1][begin2, end2]归并//归并主体}gap *= 2;//归并完后,下一趟归并的区间翻倍}
}

根据这串代码,我们在一个2 * gap范围内,每个gap都是一个子数组,紧接着我们就可以对两个子数组[begin1, end1][begin2, end2]归并。

但是这样会产生一个问题,那就是数组的尾部越界了。
我们不能保证每次gap的值都可以被数组整除,所以最后一段gap是有可能会越界的,这要如何控制?

我们一一分析:

对于begin1

由于begin1 = ii < n,所以begin1一定不可能越界。

对于end1 begin2

我们每次归并时,会得到两个已经有序的子区间[begin1, end1][begin2, end2],如果end1 begin2越界,可以理解为[begin2, end2]整个区间都越界了,而[begin1, end1]尚未越界。但是[begin1, end1]是一个已经有序的子区间,所以此时可以不用归并了,直接break,跳过本趟归并。

对于end2

end2越界,相当于是子区间[begin2, end2]有一部分在数组中,有一部分越界。而存在于数组中的那一部分就是[begin2, n - 1],所以此时我们需要将end2的值改为n-1
让区间[begin1, end1][begin2, n - 1]进行归并。

综上我们的非递归大体骨架如下:

void MergeSortNonR(int* a, int n)
{int gap = 1;//第一趟归并,每个子区间长度为1while (gap < n)//当gap超过n,说明数组合并完毕了{for (int i = 0; i < n; i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;//防止越界处理if (end1 > n || begin2 > n){break;}if (end2 > n){end2 = n - 1;}//归并主体}gap *= 2;//归并完后,下一趟归并的区间翻倍}
}

而归并主体部分已经讲解过,就是利用两个指针,每次取出最小的元素插入到新的数组中,再将新数组拷贝回去。

总代码如下:

void MergeSortNonR(int* a, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);if (tmp == NULL){perror("malloc fali!");return;}int gap = 1;while (gap < n){for (int i = 0; i < n; i += 2 * gap){int begin1 = i, end1 = i + gap - 1;int begin2 = i + gap, end2 = i + 2 * gap - 1;if (end1 > n || begin2 > n){break;}if (end2 > n){end2 = n - 1;}int j = begin1;while (begin1 <= end1 && begin2 <= end2){if (a[begin1] < a[begin2]){tmp[j++] = a[begin1++];}else{tmp[j++] = a[begin2++];}}while (begin1 <= end1){tmp[j++] = a[begin1++];}while (begin2 <= end2){tmp[j++] = a[begin2++];}memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));}gap *= 2;}free(tmp);
}

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

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

相关文章

ATA-1222A宽带放大器在二极管测试中的应用有哪些

宽带放大器是一种用于放大高频信号的电子设备&#xff0c;它在二极管测试中有多种应用。下面安泰电子将介绍宽带放大器在二极管测试中的几种常见应用。 宽带放大器可以用于二极管参数测试。二极管是一种常见的半导体器件&#xff0c;有正向电压-电流特性和反向电压-电流特性。为…

【2.5操作系统】数据传输控制方式

目录 1.输入输出技术2.IO设备管理软件 1.输入输出技术 cpu控制&#xff08;主存/外设&#xff09;进行数据交互的过程。 中断处理操作过程&#xff1a; 例题一: 解析&#xff1a; 第一问&#xff1a;选D。中断需要cpu发送中断指令。 例题二&#xff1a; 解析&#xff1a; 第…

css-实现溢出内容转换为...格式

代码&#xff1a;overflow: hidden; text-overflow: ellipsis; overflow: hidden;//当容器中的内容超出容器的尺寸时&#xff0c;将隐藏超出部分而不显示滚动条。 display: -webkit-box;//使用WebKit引擎的浏览器&#xff08;如Chrome和Safari&#xff09;中&#xff0c;将容器…

AI数字人短视频变现项目:打造短视频运营变现新模式

随着社交媒体和短视频平台的兴起&#xff0c;越来越多的人开始关注如何将短视频变现。在这个时代&#xff0c;创新和科技成为了推动变现模式发展的关键。AI数字人作为一种全新的创新形式&#xff0c;正在迅速进入人们的视野。本文将介绍AI数字人短视频变现项目&#xff0c;以及…

Mysql 安装通过mysql installer安装+配置环境+连接可视化工具

注意&#xff1a;不适合纯小白&#xff0c;小白建议移步别的大佬MySQL详细安装教程 目录 注意&#xff1a;不适合纯小白&#xff0c;小白建议移步别的大佬MySQL详细安装教程 前言 准备工作 一、Mysql下载 二、MySQL installer 安装以及系统环境配置 三、检验MySQL 四、可…

抖店商家怎么维护好与达人关系?2024新版维护达人思路方法

我是王路飞。 当你找到达人给你带货&#xff0c;且积累了一些达人资源之后&#xff0c;就需要维护好与达人的关系了。 毕竟找达人带货玩法的好处&#xff0c;就是长期稳定&#xff0c;他能给你带来持续的收益。 那么抖店商家应该如何维护好与达人的关系呢&#xff1f; 这篇…

【Python笔记】pip intall -e命令:让你的工程直接使用开源包的源码,可断点调试,修改源码!

最近学习MetaGPT&#xff0c;用到了 pip install -e . 安装命令&#xff0c;这个安装命令是从源代码安装包。 从源代码安装包有几个好处&#xff1a; 包内的代码是可见的&#xff0c;是白盒&#xff0c;不是黑盒&#xff0c;可以直接在项目中看源码断点调试可以直接断到源码里…

HCIA——10实验:跨路由转发。静态路由、负载均衡、缺省路由、手工汇总、环回接口。空接口与路由黑洞、浮动静态。

学习目标&#xff1a; 跨路由转发、负载均衡、环回接口、手工汇总、缺省路由、空接口与路由黑洞、浮动静态 学习内容&#xff1a; 跨路由转发静态路由、负载均衡、缺省路由、手工汇总。环回接口空接口与路由黑洞、浮动静态 目录 学习目标&#xff1a; 学习内容&#xff1a…

无/自监督去噪(1)——一个变迁:N2N→N2V→HQ-SSL

目录 1. 前沿2. N2N3. N2V——盲点网络&#xff08;BSNs&#xff0c;Blind Spot Networks&#xff09;开创者3.1. N2V实际是如何训练的&#xff1f; 4. HQ-SSL——认为N2V效率不够高4.1. HQ-SSL的理论架构4.1.1. 对卷积的改进4.1.2. 对下采样的改进4.1.3. 比N2V好在哪&#xff…

电影《潜行》中说的蜜罐是什么(网络安全知识)

近期刘德华、彭于晏主演的电影《潜行》在网上掀起了轩然大波&#xff0c;电影中有提到网络蜜罐&#xff0c;这引起了很多观众的疑问&#xff0c;蜜罐到底是什么&#xff1f; 从字面意思上来看&#xff0c;蜜罐就是为黑客设下的诱饵。这是一种具有牺牲性质的计算机系统&#xff…

即将推出的 OpenWrt One/AP-24.XY:OpenWrt 和 Banana Pi 合作路由器板

OpenWrt开发人员正在与Banana Pi合作开发OpenWrt One/AP-24.XY路由器板。OpenWrt 是一个轻量级嵌入式 Linux 操作系统&#xff0c;支持近 1,800 个路由器和其他设备。然而&#xff0c;这将是第一块由 OpenWrt 直接开发的路由器板。 该主板将基于 MediaTek MT7981B (Filogic 82…

Linux下的shell命令执行set -ex 错误

shell脚本&#xff1a; #!/bin/bashset -exexport GOPATH/go ...(略)执行命令报错&#xff1a; $ ./build.sh : invalid option 2: set: - set: usage: set [-abefhkmnptuvxBCHP] [-o option-name] [--] [arg ...] ./build.sh: line 3: $\r: command not found ./build.sh: l…

.NET国产化改造探索(三)、银河麒麟安装.NET 8环境

随着时代的发展以及近年来信创工作和…废话就不多说了&#xff0c;这个系列就是为.NET遇到国产化需求的一个闭坑系列。接下来&#xff0c;看操作。 上一篇介绍了如何在银河麒麟操作系统上安装人大金仓数据库&#xff0c;这篇文章详细介绍下在银河麒麟操作系统上安装.NET8环境。…

JS-var 、let 、 const使用介绍

变量声明介绍 在我们日常开发用&#xff0c;变量声明有三个 var、 let 和 const&#xff0c;我们应该用那个呢&#xff1f; 首先var 先排除&#xff0c;老派写法&#xff0c;问题很多&#xff0c;可以淘汰掉…let or const ?建议&#xff1a; const 优先&#xff0c;尽量使…

GPT-4 现在是否已经足够划算?

我通常使用 GPT 的方式是&#xff0c;先用 GPT-4 来快速搭建一个原型&#xff0c;然后不断优化&#xff0c;直到解决方案能够在 GPT-3.5 模型上运行。 这个方法在我的实践中非常高效&#xff0c;它的一个重要好处是能迅速筛选出那些“行不通”的项目——如果你在几天内都无法使…

Unity3D学习之UI系统——GUI

文章目录 1. 前言2. 工作原理和主要作用3. 基础控件3.1 重要参数及文本和按钮3.1.1 GUI 共同点3.1.2 文本控件3.1.3 按钮控件 3.2 多选框和单选框3.2.1 多选框3.2.2 单选框3.2.3 输入框3.2.4 拖动条 3.3 图片绘制和框3.3.1 图片3.3.2 框绘制 4 工具栏和选择网格4.1 工具栏4.2 选…

JWT 入门级教程

下面内容参考文章JWT详细讲解(保姆级教程)-阿里云开发者社区 (aliyun.com) 1.什么是JWT&#xff1f; JSON Web Token&#xff08;JWT&#xff09; 是一种开放标准 &#xff08;RFC 7519&#xff09;&#xff0c;它定义了一种紧凑且独立的方式&#xff0c;用于作为 JSON 对象在…

Angular系列教程之路由守卫

文章目录 前言路由守卫的类型CanLoadCanActivateCanActivateChildCanDeactivateResolve总结 前言 在Angular中&#xff0c;路由守卫是一个非常有用的功能&#xff0c;可以帮助我们控制用户在导航过程中的权限和访问限制。通过使用路由守卫&#xff0c;我们可以拦截导航并根据需…

python PyQt5的学习

一、安装与配置 1、环境&#xff1a; python3.7 2、相关模块 pip install pyqt5 pyqt5-tools pyqt5designer 可以加个镜像 -i https://pypi.tuna.tsinghua.edu.cn/simple3、配置设计器 python的pyqt5提供了一个设计器&#xff0c;便于ui的设计 界面是这样的&#xff1a…

MFC为资源对话框添加消息处理函数和初始化控件

现在我VC6新建了一个对话框工程&#xff1b;又在资源添加了一个新的对话框&#xff0c;并为新的对话框添加了名为CTestDlg的类&#xff1b; 在主对话框的cpp文件包含#include "TestDlg.h"&#xff1b; 在主对话框的cpp文件的OnInitDialog()成员函数中&#xff0c;添…