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

news2025/4/21 17:59:43

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

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


归并思想

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

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

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

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

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

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

这样我们又得到了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;//第一趟归并,每个子区间长度为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;

			//防止越界处理
			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.coloradmin.cn/o/1391543.html

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

相关文章

码云星辰:未来运维的技术交响曲

&#x1f6a9;本文介绍 ​ 随着信息技术的迅猛发展&#xff0c;运维领域正经历着翻天覆地的变革。未来的运维工程师将需要拥有更广泛、更深入的技能&#xff0c;以适应日益复杂和多变的系统环境。本文将深入探讨运维未来的行业发展趋势&#xff0c;并详细分析需要掌握的关键技…

java web万能模板(附带防微博源码)

文章目录 java EE 项目万能模板套用创建一个java EE项目1.点击java ee2.点击finish3.一个Java ee项目创建完毕4.点击运行5.运行结果 万能模板连接数据库1.在pom.xml文件导入数据库连接所需要的依赖2.进行导入3. 导入以后刷新Maven4.找到 resources 文件。5.在resources的文件路…

【论文阅读笔记】4篇Disentangled representation learning用于图像分割的论文

4篇应用解耦表示学习的文章&#xff0c;这里只关注如何解耦&#xff0c;更多细节不关注&#xff0c;简单记录一下。 1.Robust Multimodal Brain Tumor Segmentation via Feature Disentanglement and Gated Fusion Chen C, Dou Q, Jin Y, et al. Robust multimodal brain tum…

trucksim与simulink联合仿真基于pid控制算法实现车道保持

一、pid算法原理不做过多解释自行百度&#xff0c;这里采用的是位置式pid。 效果视频&#xff1a;https://www.bilibili.com/video/BV1CN4y1p7gb/?vd_sourcea1425ad8eaf3586e891a6d0040eb89cc 二、trucksim界面操作截图 三、simulink模型截图 采用的P &#xff1b; I和…

NG+WAF实现应用安全访问

一、基本概念 什么是waf&#xff1f; Web应用防火墙&#xff08;waf&#xff09;是通过执行一系列针对HTTP/HTTPS的安全策略来专门为Web应用提供保护的一款产品&#xff0c;WAF是一种工作在应用层的、通过特定的安全策略来专门为Web应用提供安全防护的产品。 什么是ngx_lua_…

SparkSQL——DataFrame

DataFrame Dataframe 是什么 DataFrame 是 SparkSQL中一个表示关系型数据库中 表的函数式抽象, 其作用是让 Spark处理大规模结构化数据的时候更加容易. 一般 DataFrame可以处理结构化的数据, 或者是半结构化的数据, 因为这两类数据中都可以获取到 Schema信息. 也就是说 DataFra…

2018年认证杯SPSSPRO杯数学建模B题(第一阶段)动态模糊图像全过程文档及程序

2018年认证杯SPSSPRO杯数学建模 B题 动态模糊图像 原题再现&#xff1a; 人眼由于存在视觉暂留效应&#xff0c;所以看运动的物体时&#xff0c;看到的每一帧画面都包含了一段时间内 (大约 1/24 秒) 的运动过程&#xff0c;所以这帧画面事实上是模糊的。对电影的截图来说&…

eNSP学习——终端直连三层网关设备进行通信

VLAN 配置 一 . 功能简介 将设备中的某些接口定义为一个单独的区域&#xff0c;将指定接口加入到指定 VLAN 中之后&#xff0c;接口就可以转发 指定 VLAN 报文。从而实现 VLAN 内的主机可以直接通信&#xff0c;而 VLAN 间的主机不能直接互通&#xff0c;将广播报文 …

前端安全相关

请求后端接口必须带上sign 以上主要是解决&#xff1a;除了数据泄露外&#xff0c;一些重要功能的接口如果没有做好保护措施也会被恶意调用造成DDoS、条件竞争等攻击效果 一些营销活动类的Web页面&#xff0c;领红包、领券、投票、抽奖等活动方式很常见。此类活动对于普通用户…

Linux miniGUI移植分析

框架介绍 常用GUI程序对比 https://www.cnblogs.com/zyly/p/17378659.html MiniGUI分为底层的GAL&#xff08;图形抽象层&#xff09;和IAL&#xff08;输入抽象层&#xff09;&#xff0c;向上为基于标准POSIX接口中pthread库的Mini-Thread架构和基于Server/Client的Mini-L…

WebGL中开发AR应用

WebGL在本质上是用于在浏览器中进行3D和2D图形渲染的技术&#xff0c;而增强现实&#xff08;AR&#xff09;通常需要与现实世界的环境进行交互。要在WebGL中开发AR应用&#xff0c;您可以采取以下步骤&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专…

固定拍摄点位下的NeRF技术应用(算法探讨)

NeRF&#xff08;神经辐射场&#xff09;技术可以用于物体定位。NeRF 是一种深度学习方法&#xff0c;它通过对场景的大量照片进行训练来创建三维场景的高度逼真的渲染。这项技术能够从多个角度捕捉场景的细节&#xff0c;并通过神经网络理解场景的三维结构。 使用 NeRF 进行物…

容器部署的nextcloud配置onlyoffice时开启密钥

容器部署的nextcloud配置onlyoffice时开启密钥 配置 进入onlyoffice容器 docker exec -it 容器id bash编辑配置vi /etc/onlyoffice/documentserver/local.json enable设置为true&#xff0c;并配置secret 重启容器&#xff0c;并将配置的密钥填入nextcloud密钥页面 docker r…

协方差矩阵自适应调整的进化策略(CMA-ES)

关于CMA-ES&#xff0c;其中 CMA 为协方差矩阵自适应(Covariance Matrix Adaptation)&#xff0c;而进化策略&#xff08;Evolution strategies, ES&#xff09;是一种无梯度随机优化算法。CMA-ES 是一种随机或随机化方法&#xff0c;用于非线性、非凸函数的实参数&#xff08;…

【每日小bug】mybatis plus id注解错误导致的问题

插入数据 id不为自增 指定了主键&#xff0c;没有指定自增。会导致出现 修改如上 报错 Data truncation: Out of range value for column ‘id’ at row 1 数据库是bigint&#xff0c;java中是Integer。 修改如上

现代工程科技杂志现代工程科技杂志社现代工程科技编辑部2023年第21期目录

能源科技 配网故障停电原因及改进对策研究 上官安琪 110kV变电站电气自动化技术及应用策略 陈祥 变电运维误操作事故预控措施分析 高翔;韦婉 智能变电站变电运维安全与设备维护探究 温亮亮;覃万全 110kV变电站电气设计及其防雷保护案例研析 谢旭平 变电运维…

springboot-简单测试 前端上传Excel表格后端解析数据

导入依赖 <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>5.2.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxm…

力扣白嫖日记(sql)

前言 练习sql语句&#xff0c;所有题目来自于力扣&#xff08;https://leetcode.cn/problemset/database/&#xff09;的免费数据库练习题。 今日题目&#xff1a; 610.判断三角形 表&#xff1a;Triangle 列名类型xintyintzint 在sql中&#xff0c;(x,y,z)是该表的主键列…

微信小程序---如何创建分包

1.在项目根目录中&#xff0c;创建分包的根目录&#xff0c;名为subpkg&#xff0c;这个名字可以自己定义 2.在 pages.json 中&#xff0c;和 pages 节点平级的位置声明 subPackages 节点&#xff0c;用来定义分包相关的结构&#xff1a; 3.在分包目录&#xff0c;点击右键新建…

变革在即:为什么您现在就需要将大型语言模型融入系统与产品开发

“有些问题看起来易于构想和展示原型&#xff0c;但将其发展成实际产品却极为艰难。比如自动驾驶&#xff1a;展示一辆汽车在街区自动行驶很简单&#xff0c;但要把这一技术转化为成熟的产品却需要十年时间。” - Karpathy 本文旨在探讨如何将大语言模型&#xff08;LLMs&#…