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

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

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


归并思想

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

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

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

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

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

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

这样我们又得到了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);
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java数据结构实现数组(配套习题)

数据结构 数组 一组相同数据类型的集合 特点 数组在内存中是连续分配的创建时要指明数组的大小数组名代表首地址,索引从0开始,到数组的长度-1数组一旦创建好,大小不可以改变使用索引 获取索引位置的值 arr[index]修改 arr[index] val删除 (假删除)遍历,将数组中的元素,依次…

VMware虚拟机自定义网段及物理机ping不通虚拟机问题解决

Vmware网络介绍&#x1f6dc; VMware虚拟机提供了几种网络模式&#xff0c;其中包括桥接模式&#xff08;Bridged Mode&#xff09;、NAT模式&#xff08;Network Address Translation Mode&#xff09;和仅主机模式&#xff08;Host-Only Mode&#xff09;。这些模式允许虚拟…

掌握Spring缓存-全面指南与最佳实践

第1章&#xff1a;引言 大家好&#xff0c;我是小黑&#xff0c;咱们今天来聊聊缓存&#xff0c;在Java和Spring里&#xff0c;缓存可是个大角色。咱们在网上购物&#xff0c;每次查看商品详情时&#xff0c;如果服务器都要去数据库里翻箱倒柜&#xff0c;那速度得慢成什么样&…

【计算机网络】网络层——详解IP协议

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【网络编程】 本专栏旨在分享学习计算机网络的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 &#x1f431;一、I…

.Net Core 使用 AspNetCoreRateLimit 实现限流

上一篇文章介绍过ASP.NET Core 的 Web Api 实现限流 中间件-CSDN博客 使用.NET 7 自带的中间件 Microsoft.AspNetCore.RateLimiting 可以实现简单的Api限流&#xff0c;但是这个.NET 7以后才集成的中间件&#xff0c;如果你使用的是早期版本的.NET&#xff0c;可以使用第三方库…

腊八蒜怎么腌制才能又脆又绿 把这招记在备忘录一步步制作

过了腊八就是年&#xff0c;这句话像是一个温暖的预告&#xff0c;告诉我们新年即将到来。而在我家的年味里&#xff0c;总少不了一瓶瓶翠绿的腊八蒜。每当亲朋好友围坐在一起&#xff0c;那独特的蒜香总能为我们的欢聚时光增添几分风味。 腌制腊八蒜是个技术活&#xff0c;很…

Git Merge、Rebase 和 Squash 之间的区别

文章目录 Git MergeGit RebaseGit Squash结论 作为一名开发人员&#xff0c;您可能使用过 Git 和 GitHub&#xff0c;掌握了版本控制的要点。通常通过拉取请求将分支的更改集成到主分支中是一项常见任务。许多人的默认选择是“合并”功能。 然而&#xff0c;版本控制领域提供了…

论文笔记:信息融合的门控多模态单元(GMU)

整理了GMU&#xff08;ICLR2017 GATED MULTIMODAL UNITS FOR INFORMATION FUSION&#xff09;论文的阅读笔记 背景模型实验 论文地址&#xff1a; GMU 背景 多模态指的是同一个现实世界的概念可以用不同的视图或数据类型来描述。比如维基百科有时会用音频的混合来描述一个名人…

项目解决方案:“ZL铁路轨行车辆”实时视频监控系统

目 录 一、建设背景 1.1 政策背景 1.2 现状 二、建设目标 三、建设依据 四、建设原则 4.1经济高效性 4.2系统开放性 4.3系统继承性 4.4系统扩展性 4.5系统经济性 4.6系统安全性 五、系统架构 5.1系统架构图 5.2技术架构 1、DVS 2、中心管理服务…

测试的基本概念

1、什么是需求&#xff1f; 在企业中主要分为两类&#xff1a;用户需求和软件需求 用户需求&#xff1a;甲方的需求&#xff0c;或者终端用户使用产品时必须要完成的任务&#xff08;比较简略&#xff09;。 软件需求&#xff1a;或者叫功能需求&#xff0c;该需求会详细描述开…

Qt单个字符判断

1.相关说明 字符的Unicode编码、单个字符的判断 2.界面绘制 3.相关主要代码 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this); }Widget::~Widget() {delete ui;…

数学建模常见算法的通俗理解(更新中)

目录 1.层次分析法&#xff08;结合某些属性及个人倾向&#xff0c;做出某种决定&#xff09; 1.1 粗浅理解 1.2 算法过程 1.2.1 构造判断矩阵 1.2.2 计算权重向量 1.2.3 计算最大特征根 1.2.4 计算C.I.值 1.2.5 求解C.R.值 1.2.6 判断一致性 1.2.7 计算总得分 2 神经…

MySQL 多版本并发控制 MVCC

MVCC出现背景 事务的4个隔离级别以及对应的三种异常 读未提交&#xff08;Read uncommitted&#xff09; 读已提交&#xff08;Read committed&#xff09;&#xff1a;脏读 可重复读&#xff08;Repeatable read&#xff09;&#xff1a;不可重复读 串行化&#xff08;Se…

pygame学习(三)——支持多种类型的事件

大家好&#xff01;我是码银&#x1f970; 欢迎关注&#x1f970;&#xff1a; CSDN&#xff1a;码银 公众号&#xff1a;码银学编程 实时事件循环 为了保证程序的持续刷新、保持打开的状态&#xff0c;我们会创建一个无限循环&#xff0c;通常使用的是while语句&#xff0c;w…

嵌出式学习又一天

关于485通讯 485属于串口通信&#xff0c;属于物理层的&#xff0c;规定为2线&#xff0c;半双工的多点通信标准&#xff0c;它的电气特性不一样&#xff0c;用缆线两端电压差值来表示传递信号&#xff0c;rs485仅仅规定了接收端和发送端的电气特性&#xff0c;没有规定任何数据…

esp32-idf Eclipse Log日志打印demo

Log日志打印demo 1、代码例程 esp32-S2 芯片 / Eclipse软件 开发环境 #include <stdio.h> #include "sdkconfig.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "esp_system.h" #include "esp_…

数据分析求职-知识脑图

今天和大家聊聊数据分析求职常见面试题&#xff0c;这是这个系列的第一篇文章&#xff0c;但是我不想开始就直接罗列题目&#xff0c;因为这样的文章实在太多了&#xff0c;同学们的兴趣程度肯定一般。所以&#xff0c;我想先和大家聊聊在准备面试题时候通常遇到的困扰&#xf…

7.5 MySQL对数据的增改删操作(❤❤❤)

7.5 MySQL对数据的基本操作 1. 提要2. 数据添加2.1 insert语法2.2 insert 子查询2.3 ignore关键字 3. 数据修改3.1 update语句3.2 update表连接 4. 数据删除4.1 delete语句4.2 delete表连接4.3 快速删除数据表全部数据 1. 提要 2. 数据添加 2.1 insert语法 2.2 insert 子查询 …

为什么 macOS 比 Windows 稳定?

在计算机操作系统领域&#xff0c;macOS 和 Windows 分别是苹果公司和微软公司的主打产品。尽管两者都拥有大量的用户群体&#xff0c;但在稳定性和用户体验方面&#xff0c;macOS 常常被认为优于 Windows。那么&#xff0c;为什么 macOS 比 Windows 更稳定呢&#xff1f; 我们…