【C语言】归并排序递归和非递归——动图演示

news2024/11/15 20:26:29

目录

  • 一、归并排序思想
    • 1.1 基本思想
    • 1.2 大体思路
  • 二、实现归并排序(递归)
  • 三、实现归并排序(非递归)
    • 3.1 实现思路:
    • 3.2 越界处理
    • 3.3 时间复杂度和空间复杂度
  • 总结


一、归并排序思想

1.1 基本思想

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法
(Divide and Conquer)的一个非常典型的应用。
已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并
总得来说就是先把子序列有序,在让有序的子序列合并成一个有序序列

在这里插入图片描述

1.2 大体思路

  1. 将数据从中间分成两个子序列
  2. 将子序列不断的划分,直到只剩一个数据,该子序列有序了
  3. 子序列有序了,就开始两两一合并,直到所有的子序列合并完,排序就完成了。
    在这里插入图片描述

二、实现归并排序(递归)

思路

  • 前提:
    申请一个临时数组tmp,因为我们不能在原数组归并,不然会造成覆盖,需要在临时归并,在拷回原数组里。
    由于创建了临时数组,我们就需要在创建一个子函数用来专门递归,不然每次调用都会申请空间

  • 子函数思路:

  1. 将序列从中间分成两个区间[left,mid] [mid + 1,right],调用递归,直到只剩一个数据,一个数据就表示有序了。
  2. 在把递归的两个区间进行合并,begin1和end2表示左序列,begin1和end2表示右序列,比较两个序列的值插入到临时数组里,其中肯定会有一个先结束,当其中一个序列拷贝完了就停止拷贝。
  3. 把剩下未合并的数据,全部放到tmp数组里。
  4. 最后在tmp数组的数据拷到原数组里。
// 子函数:用来递归
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;
	
	int mid = (left + right) >> 1;

	// 分区[left, right] -> [left, mid] [mid + 1, right]
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	// 标识归并两个有序的开始和结束
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;
	
	// 两个序列合并,肯定会有一个先结束
	int index = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}

	// 处理剩下未合并完的数据,合并完的肯定不会进入循环
	while (begin1 <= end1)
		tmp[index++] = a[begin1++];

	while (begin2 <= end2)
		tmp[index++] = a[begin2++];

	// tmp的数据拷贝到原数组,闭区间所以拷贝个数要加1
	memmove(a + left, tmp + left, (right-left+1) * sizeof(int));
}


void MergeSort(int* a, int n)
{
	// 创建临时数组,用来存放合并完的数据
	int* tmp = (int*)malloc(n * sizeof(int));
	_MergeSort(a, 0, n - 1, tmp);
	
	free(tmp);
}

细节注意:

  • 归并的时位置都是相对位置,如[2,2][3,3]合并,合并完存放在tmp数组里也要是[2,3],所以两个序列都是两个区间的相对位置,所以左序列begin1 = left,end1 = mid右序列begin2 = mid+1,end2 = right
  • index = left,不是index = 0,也是为了将原数组的相对位置的数据合并到tmp数组的相对位置,如a[2]就要放到tmp[2]
  • 拷贝数据函数memmove使用,memmove(目的地址dst源地址src拷贝数据个数(单位字节)
  • 同样拷贝数据时,也要注意相对位置,如memmove(a, tmp, (right-left+1) * sizeof(int)),这样每次就会从tmp下标0开始拷贝,并不是相对位置。
  • 子函数使用的闭区间,传的是拷贝数据个数,需要+1,如:[0,9],一个10个数

三、实现归并排序(非递归)

递归还是那个栈溢出问题,上一篇快排非递归版本详细说明和举例过了,这里不在一一说明了。快速排序

在这里插入图片描述

gap表示每组数据的个数,一次两组数据归并,得出2*gap就是一次归并的个数,通过每组个数(gap)推导出每组数据的区间,如[0,0]和[1,1]归并,gap为1,i = 0,得出[i,i + gap - 1][i + gap,i + 2*gap - 1]
排下一次就需要i += 2 * gap,就可以访问下一次归并的起始位置了,如果先是[0,0]和[1,1]归并,在就是[2,2]和[3,3]归并
归并好的数据需要放入到临时数组大家可以通过上图代入值。

这是只是单趟排好多组的样子,这里模拟递归最后一层的情况,和递归相反,我们是从直接最后一层开始归并,递归还需要慢慢的分区递归下来开始归并。
想要整体排好序,gap每次就乘以2,直到gap为n/2,就是说把未排序的数据分成两组数据,两组数据归并完就排好了序。

每次单趟归并完就把临时数组的数据拷回原数组里,再用原数据归并,直到排好序,如下图
在这里插入图片描述

3.1 实现思路:

  1. 同样也需要创建一个临时数组tmp,不能在原数据归并会覆盖。
  2. gap为每组数据个数,先从gap为1,一次两组数据归并,模拟递归最后一层的归并,如:[0,0]和[1,1]归并
  3. 通过每组个数(gap)推导出每组数据的区间,得出两组数据区间是[i,i + gap - 1][i + gap,i + 2*gap - 1]
  4. i需要加上2*gap,原来的区间已归并好了,所以需要指向下一次归并的起始位置,直到 i >= n结束,循环条件就要设为i < n,表示排好差距为gap的多组数据。
  5. 排好差距为gap多组数据后,gap*2,下一趟需下一趟归并的一组数据个数翻倍,直到gap为gap >= n结束,>=就只有一个组数据,不能归并,循环条件就是gap < n,就是说把未排序的数据分成两组数据,两组数据归并完就排好了序。
  6. 把临时数组的数据拷到原数组。
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(n * sizeof(int));
	assert(tmp);

	int gap = 1;	// 每组的数据个数
	
	// gap < n,就说明有两组数据才能归并
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			// [i, i + gap - 1] [i + gap, i + gap * 2 - 1]
			// 如:[0, 0] 和 [1, 1]......
			// 标识需归并两组数据的范围
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;

			// 归并
			int index = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			// 剩余未归并完的数据全部放入tmp里
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
		}
		// 当前gap多组都归并完了,在全部拷过去(一次拷全部)
		memmove(a, tmp, n * sizeof(int));

		gap *= 2;	// 下一趟归并的一组数据个数翻倍
	}
	free(tmp);
}

3.2 越界处理

上面的代码,我正好拿2的指数,8个数据测试的,如果不是2的指数, 2 3 2^3 23 2 4 2^4 24等…,就会越界,分组无法平分,肯定会导致一方越界,一共有3种越界。

在这里插入图片描述
越界情况:

  1. 右区间不存在
  2. 左区间缺少值
  3. 右区间超过数据长度

越界情况1和2可以只需处理一个右区间不存在就可以了,为什么呢?

右区间不存在的话,包含了左区间缺少值的情况,左区间同时也是在原数组中是有序的,所以直接跳出不用拷贝,只拷贝归并了的数据。

越界情况3:只需要纠正右区间的结束标识,让结束标识到最后一个数据即可。

处理:

  1. 右区间不存在,直接跳出循环break,不进行归并。
  2. 右区间超过数据长度,进行修正,右区间结束标识n-1,就是最后一个数据的下标。
  3. 由于右区间不存在直接跳出的情况,tmp数组就会有随机值情况,并且左区间在原数组中也是有序的。
    上面代码是把当前差距为gap的数据,全部归并好,在全部拷贝到原数据中。
    这里就要修改成归并了多少,就拷多少,归并完直接拷贝,需要放进循环里面。

补充说明:
拷贝数据也可以在全部归并好,在全部拷到原数据,只不过越界处理很麻烦,并且没有归并了多少,拷多少来的直观。
在这里插入图片描述
在这里插入图片描述

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(n * sizeof(int));
	assert(tmp);

	int gap = 1;	// 每组的数据个数
	
	// gap < n,就说明有两组数据才能归并,>=就说明只有一组数据
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			// [i, i + gap - 1] [i + gap, i + gap * 2 - 1]
			// 如:[0, 0] 和 [1, 1]......
			// 标识需归并两组数据的范围
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;

			// 右区间不存在
			if (begin2 >= n)
				break;

			// 右区间超过了数据长度,修正成数据的长度
			if (end2 >= n)
				end2 = n - 1;

			// 归并
			int index = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{	
					tmp[index++] = a[begin2++];
				}
			}

			// 剩余未归并完的数据全部放入tmp里
			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}

			// 拷贝数据,归并了多少,就拷多少
			for (int j = i; j <= end2; j++)
			{
				a[j] = tmp[j];
			}
		}
		gap *= 2;	// 下一趟归并的一组数据个数翻倍 
	}
}

3.3 时间复杂度和空间复杂度

在这里插入图片描述
**分解:**在每一层递归中,都需要将数组分成两个子数组,因此递归树的深度为logn。
**合并:**在每一层递归中,需要将两个有序子数组合并成一个有序数组,这个操作的时间复杂度为O(n)。
因此总的时间复杂度为O(nlogn)。

空间复杂度

需要申请一个临时数组tmp,长度和原数组一样大。
所以空间复杂度为 O ( N ) O(N) O(N)


总结

归并排序的时间复杂度是 O ( n ∗ l o g n ) O(n*logn) O(nlogn),空间复杂度是 O ( n ) O(n) O(n)

归并排序非递归的越界处理是难点,需要多加画图和调试分析情况,才能很好控制越界问题。

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

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

相关文章

RTMP和WebRTC使用场景有哪些差别?

省流版先说结论 直播领域&#xff0c;RTMP和WebRTC各有优势。如果直播场景对延迟有一定要求&#xff0c;但更注重稳定性和兼容性&#xff0c;那么RTMP可能是一个更好的选择。如果直播场景需要极低的延迟&#xff0c;并且用户主要在浏览器环境下进行观看和互动&#xff0c;那么…

Leangoo敏捷工具在缺陷跟踪(BUG)管理中的高效应用

在开发过程中&#xff0c;缺陷&#xff08;BUG&#xff09;管理一直是项目管理中的一个关键环节。及时发现并修复BUG&#xff0c;不仅能够提高产品质量&#xff0c;还能有效提升团队的工作效率和用户满意度。 在敏捷开发中&#xff0c;快速迭代和频繁交付的特点使得缺陷管理的…

Servlet的特性(一)

Servlet的主要用途&#xff1a; 接受、处理来自浏览器端(BS架构中的B端)的请求和用户输入 响应来自数据库或者服务端(BS架构中的S端)产生的数据到浏览器端&#xff0c;动态构建网页。 手动实现Servlet小程序 实现步骤 自定义一个类型&#xff0c;实现Servlet接口或者继承Ht…

Spring Boot 集成 Redisson 实现消息队列

包含组件内容 RedisQueue&#xff1a;消息队列监听标识RedisQueueInit&#xff1a;Redis队列监听器RedisQueueListener&#xff1a;Redis消息队列监听实现RedisQueueService&#xff1a;Redis消息队列服务工具 代码实现 RedisQueue import java.lang.annotation.ElementTyp…

GD32E230 RTC报警中断功能使用

GD32E230 RTC报警中断使用 GD32E230 RTC时钟源有3个&#xff0c;一个是内部RC振动器产生的40KHz作为时钟源&#xff0c;或者是有外部32768Hz晶振.,或者外部高速时钟晶振分频作为时钟源。 &#x1f516;个人认为最难理解难点的就是有关RTC时钟异步预分频和同步预分频的计算。在对…

C++第二节入门 - 缺省参数和函数重载

一、缺省参数 1、概念 缺省参数是声明或定义函数时为函数的参数指定一个缺省值。 在调用该函数的时候&#xff0c;如果没有指定实参则采用该形参的缺省值&#xff0c;否则使用指定的实参&#xff01; #include<iostream> using namespace std;void Func(int a 0) {c…

2024 水博会,国信华源登场,数智创新助力水利高质量发展

9月4日-6日&#xff0c;由中国水利学会和中国水利工程协会共同主办的2024中国水博览会暨第十九届中国&#xff08;国际&#xff09;水务创新技术交流会在重庆国际博览中心召开。 本次水博会以“展水利前沿新技术 览新质生产力场景”为主题&#xff0c;国信华源携最新智能监测预…

【佳学基因检测】如何升级一个不再维护的软件包中的PHP代码?

如何升级一个不再维护的软件包中的PHP代码&#xff1f; 为什么要升级一个不再维护但是仍在使用的软件包中的PHP代码&#xff1f; 升级一个不再维护但仍在使用的软件包中的 PHP 代码是一个复杂但重要的过程。虽然这些软件包可能已经不再活跃地维护或更新&#xff0c;但升级其代…

通信工程学习:什么是ATM异步转移模式

ATM&#xff1a;异步转移模式 ATM&#xff1a;Asynchronous Transfer Mode&#xff08;异步转移模式&#xff09;是一种先进的通信技术&#xff0c;它采用固定长度的信元&#xff08;Cell&#xff09;作为信息传输、复用、交换及处理的基本单位&#xff0c;并通过异步时分复用的…

挖矿木马-Linux

目录 介绍步骤 介绍 1、挖矿木马靶机中切换至root用户执行/root目录下的start.sh和attack.sh 2、题目服务器中包含两个应用场景&#xff0c;redis服务和hpMyAdmin服务&#xff0c;黑客分别通过两场景进行入侵&#xff0c;入侵与后续利用线路路如下&#xff1a; redis服务&…

Tomcat Request Cookie 丢失问题

优质博文&#xff1a;IT-BLOG-CN 一、问题描述 生产环境偶尔(涉及到多线程处理)出现"前端传递Cookie为空"的告警&#xff0c;导致前端请求丢失&#xff0c;出现请求失败问题。告警内容如下 前端传递Cookie为空 告警内容&#xff1a;服务端获取request Cookie为空&…

2024网安周今日开幕,亚信安全亮相30城

2024年国家网络安全宣传周今天在广州拉开帷幕。今年网安周继续以“网络安全为人民&#xff0c;网络安全靠人民”为主题。2024年国家网络安全宣传周涵盖了1场开幕式、1场高峰论坛、5个重要活动、15场分论坛/座谈会/闭门会、6个主题日活动和网络安全“六进”活动。亚信安全出席20…

每日一练:螺旋矩阵

一、题目要求 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5]示例 2&#xff1a; 输入&#xff1a;ma…

Java | Leetcode Java题解之第396题旋转函数

题目&#xff1a; 题解&#xff1a; class Solution {public int maxRotateFunction(int[] nums) {int f 0, n nums.length, numSum Arrays.stream(nums).sum();for (int i 0; i < n; i) {f i * nums[i];}int res f;for (int i n - 1; i > 0; i--) {f numSum - …

NISP 一级 | 3.1 网络基础知识

关注这个证书的其他相关笔记&#xff1a;NISP 一级 —— 考证笔记合集-CSDN博客 0x01&#xff1a;Internet 和 TCP/IP 协议 因特网&#xff08;Internet&#xff09;通过 TCP/IP 协议将遍布在全世界各地的计算机互联&#xff0c;从而形成超级计算机网络。因特网为用户提供了非…

【50 Pandas+Pyecharts | 暑期档电影票房数据分析可视化】

文章目录 &#x1f3f3;️‍&#x1f308; 1. 导入模块&#x1f3f3;️‍&#x1f308; 2. Pandas数据处理2.1 读取数据2.2 提取电影名称 &#x1f3f3;️‍&#x1f308; 3. Pyecharts数据可视化3.1 电影总票房排行3.2 各电影票房占比3.3 2023中国各省地区大学数量分布3.4 《抓…

浅谈产线工控安全,产线工控安全的有效方案

随着工业4.0的发展&#xff0c;产线日益智能化&#xff0c;生产网已经发展成一个组网的计算机环境&#xff0c;虽然都进行了隔离&#xff0c;但仍需和外部进行数据交互&#xff0c;导致有病毒入侵可能。 产线工控安全事件不断 深信达MCK主机加固方案&#xff0c;针对产线工控…

工业无人机性能参数特点!!!

一、基本性能参数 动力系统&#xff1a;工业无人机多采用电动或油动动力系统&#xff0c;以提供足够的推力和续航能力。电动无人机通常具有较低的噪音和振动&#xff0c;适合城市或近距离作业&#xff1b;而油动无人机则具有更长的续航时间和更大的载重能力&#xff0c;适合远…

安科瑞Acrel-1000DP分布式光伏监控系统平台的设计与应用-安科瑞 蒋静

针对用户新能源接入后存在安全隐患、缺少有效监控、发电效率无法保证、收益计算困难、运行维护效率低等通点&#xff0c;提出的Acrel-1000DP分布式光伏监控系统平台&#xff0c;对整个用户电站全面监控&#xff0c;为用户实现降低能源使用成本、减轻变压器负载、余电上网&#…

MySQL基础作业三

查询 1.分别查询student表和score表的所有记录 mysql> select *from student; ---------------------------------------------------- | id | name | sex | birth | department | address | ---------------------------------------------------- | 901 | 张三丰…