归并排序-MergeSort (C语言详解)

目录

  • 前言
  • 归并排序的思想
  • 归并排序的递归法
  • 归并排序的非递归法
  • 归并排序的时间复杂度与适用场景
  • 总结

前言

好久不见, 前面我们了解到了快速排序, 那么本篇旨在介绍另外一种排序, 它和快速排序的思想雷同, 但又有区别, 这就是归并排序, 如下图, 我们对比快速排序与归并排序.
在这里插入图片描述

本章也会深入介绍归并排序的两种写法, 递归版本的归并排序与非递归版本的归并排序.

博客主页:酷酷学!!!
您的支持是我更新的最大动力!


归并排序的思想

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

在这里插入图片描述
归并排序的步骤如下:

  1. 将待排序序列分为两个子序列,直到每个子序列只有一个元素为止。
  2. 比较两个子序列的首个元素,将较小的元素放入一个新的临时序列中。
  3. 如果其中一个子序列已经被遍历完,则将另一个子序列中剩余的元素依次放入临时序列中。
  4. 将临时序列中的元素复制回原始序列的相应位置,得到已经排序好的子序列。
  5. 重复步骤2-4,直到所有的子序列都已经归并完成。
  6. 返回最终排序好的序列。

在这里插入图片描述


归并排序的递归法

首先我们需要具备这样一个思想, 如果两组数据有序, 我们就可以进行归并, 取小的数据进行依次排列, 那么我们就需要一个临时的数组进行存放, 首先动态申请块与数组空间大小相同的空间, 然后进行内层函数的递归调用, 不能直接调用外层函数, 因为会重复申请空间.


void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	_MergeSort(a, tmp, 0, n - 1);

	free(tmp);
	tmp = NULL;
}

接着, 我们来写内层函数, 首先需要给定一段区间, 用来进行此区间的排序, 我们进行递归的调用,直至区间中只有一个元素, 我们默认它为有序, 此时就可以进行归并排序, 这里与数组和链表将两个有序链表合成一个有序链表的思路是相通的, 这也可见学习算法是具有连贯性和螺旋式上升的一个过程, 接着我们需要拷贝临时数组的数据到原数组中, 每次归并一小段区间就要拷贝一次, 以免有不必要的麻烦, 到此我们的归并排序已经完成.是不是比较简单.

void _MergeSort(int* a, int* tmp, int begin, int end)
{
	if (begin >= end)
	{
		return;
	}
	int mid = (begin + end) / 2;
	_MergeSort(a, tmp, begin, mid);
	_MergeSort(a, tmp, mid + 1, end);

	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;

	int j = begin;
	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+begin, tmp+begin, sizeof(int) * (end - begin + 1));
}

归并排序的非递归法

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		perror("malloc fail");
		return;
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap*2)
		{
			int j = i;
			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;
			}
			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);
	tmp = NULL;
}

非递归法与递归法的思路是一模一样的,只不过呈现的形式有所不同, 首先我们来回忆一下递归法的过程, 就是先递归让数组的区间元素个数为1, 认为为有序数组, 然后依次回退进行归并, 我们这里也可以这样, 首先模拟一个元素为一个区间, 进行归并, 然后两个有序元素进行归并, 然后变成四个, 八个, 直到归并区间个数大于等于N.

第一步, 我们先分gap组, gap为每组归并的个数, 比如gap为1,那么每组就归并一个数据, 我们我们先看内层for循环, 进行第一次gap为1的归并, 此时一个数据与一个数据进行归并, 归并成含有两个数据的有序数组, 此时我们的目的就完成了, 这个思想与归并排序的思路是一致的, 此时有同学会问, 为什么i要+=gap*2, 因为每次归并一段区间, 比如第一次下标为0与下标为1进行归并, 第二次下标为2和下标为3进行归并, 所以每次要跳过两个组,然后进行下两组数据的归并.

在这里插入图片描述
i代表每组的起始位置,第一次归并完成之后, 我们进行第二次归并, gap为2, 进行22归并,然后44归并, 知道gap>=n, 但是这种也存在越界风险, 比如下图

在这里插入图片描述

我们对上面一组数据进行排序,发现十个数据, 进行排序, 除了begin1不会越界, 其他都有可能越界的风险,处理方法

在这里插入图片描述

对于begin2和end1越界, 那么我们就不需要归并了, 如果end2越界, 我们只需要重新调整一下区间, 代码如下:

			if (begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}

如此非递归版的归并排序我们也完成了.


归并排序的时间复杂度与适用场景

  1. 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
  2. 时间复杂度:O(N*logN)
  3. 空间复杂度:O(N)
  4. 稳定性:稳定

对于时间复杂度我们可以理解为递归深度, 就正如二叉树类似, 不难算出时间复杂度.

适用场景:

  1. 当需要对一个较大规模的数据集进行排序时,归并排序是一种比较高效的排序算法。它的时间复杂度相对较低,且具有稳定性,不会改变相同元素的相对次序。
  2. 归并排序适用于对链表进行排序。由于链表的特殊结构,使用归并排序在空间上更加节省,且对链表进行合并操作非常方便。
  3. 当数据集的存储方式不支持随机访问时,如外部排序,归并排序也是一个很好的选择。它可以对数据集进行分块读取,然后进行排序和合并。

总结

归并排序是一种经典的排序算法,它的基本思想是将待排序的序列分成两个子序列,分别进行递归地排序,然后将两个排好序的子序列合并成一个有序序列。

归并排序的具体步骤如下:

  1. 将待排序序列不断地划分,直到每个子序列只有一个元素。
  2. 对相邻的两个子序列进行合并,得到一个有序的子序列。
  3. 不断地合并子序列,直到最终得到一个有序的序列。

归并排序的时间复杂度为O(nlogn),其中n是待排序序列的长度。它的空间复杂度为O(n),因为在合并子序列的过程中需要额外的空间来存储临时结果。

归并排序是一种稳定的排序算法,它的优点是可以保持原序列中相同元素的相对顺序不变。它适用于对大规模数据进行排序,但由于需要额外的空间,所以对于内存有限的情况下可能不太适用。

总的来说,归并排序是一种简单而高效的排序算法,它的实现也相对容易理解。在实际应用中,可以根据具体情况选择是否使用归并排序来解决排序问题。



感谢关注, 如有疑问欢迎留言, 留言必回!!!

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

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

相关文章

编译器的控制流图分析

1&#xff0c;建立感性认识 1.1 源码 hello.c int x 10; int y 11; int main(){int z 12;for (int i 0;i < 10;i){z * x * y;}if(z>7.0)z1.0f;elsez 2.0f;return 0; }1.2 编译 2005 sudo apt-get install -y graphviz-doc libgraphviz-dev graphviz2034 ../ex_…

Java学习高级一

修饰符 static 类变量的应用场景 成员方法的分类 成员变量的执行原理 成员方法的执行原理 Java之 main 方法 类方法的常见应用场景 代码块 设计模式 单例设计模式 饿汉式单例设计模式 懒汉式单例设计模式 继承 权限修饰符

LeetCode题练习与总结:二叉树的后序遍历--145

一、题目描述 给你一棵二叉树的根节点 root &#xff0c;返回其节点值的 后序遍历 。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[3,2,1]示例 2&#xff1a; 输入&#xff1a;root [] 输出&#xff1a;[]示例 3&#xff1a; 输入&#xff1a…

以太坊DApp交易量激增83%的背后原因解析

引言 最近&#xff0c;以太坊网络上的去中心化应用程序&#xff08;DApp&#xff09;交易量激增83%&#xff0c;引发了广泛关注和讨论。尽管交易费用高达2.4美元&#xff0c;但以太坊仍在DApp交易量方面遥遥领先于其他区块链网络。本文将深入探讨导致这一现象的主要原因&#…

颅内感染性疾病患者就诊指南

颅内感染性疾病&#xff0c;即病原体侵入中枢神经系统&#xff0c;导致脑部或脑膜发生炎症的疾病。这些病原体可能是细菌、病毒、真菌或寄生虫等。颅内感染不仅会对脑组织造成损害&#xff0c;还可能引发一系列严重的并发症&#xff0c;如癫痫发作、意识障碍等 颅内感染性疾病的…

国产软件号称Windows系统的天花板,却被误认为是外国佬研发

说起国产软件&#xff0c;大家总是容易给它们贴上“流氓、捆绑、满满的都是套路”这样的标签。 其实挺冤枉的&#xff0c;有些软件真的挺好用&#xff0c;也挺良心的&#xff0c;但就是因为这些刻板印象&#xff0c;老是被误以为是外国工程师搞出来的。 VeryCapture 之前小编…

JavaScript之深入对象,详细讲讲构造函数与常见内置构造函数

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;我是前端菜鸟的自我修养&#xff01;今天给大家详细讲讲构造函数与常见内置构造函数&#xff0c;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;原创不易&#xff0c;如果能帮助到带大家&#xff0c;欢迎…

达梦数据库的系统视图v$deadlock_history

达梦数据库的系统视图v$deadlock_history 在达梦数据库&#xff08;DM Database&#xff09;中&#xff0c;V$DEADLOCK_HISTORY 视图记录了数据库中发生的死锁信息。通过查询这个视图&#xff0c;数据库管理员可以监控和诊断数据库中的死锁问题&#xff0c;从而采取相应的措施…

鸿蒙认证值得考吗?

鸿蒙认证值得考吗&#xff1f; 鸿蒙认证&#xff08;HarmonyOS Certification&#xff09;是华为为了培养和认证开发者在鸿蒙操作系统&#xff08;HarmonyOS&#xff09;领域的专业技能而设立的一系列认证项目。这些认证旨在帮助开发者和企业工程师提升在鸿蒙生态中的专业技能…

小故事——半个世纪的爱情

半个世纪的爱情 故事的开端永远是在那个情窦初开的年纪&#xff0c;那富有蓬勃朝气的少年时代&#xff0c;眼神中青涩未尽&#xff0c;正是这个时间&#xff0c;才真正的让人难以忘怀。她不过是那班级里面普普通通的小孩&#xff0c;故事的男主角同样也是简简单单的存在&#…

激光SLAM如何动态管理关键帧和地图

Tip: 如果你在进行深度学习、自动驾驶、模型推理、微调或AI绘画出图等任务&#xff0c;并且需要GPU资源&#xff0c;可以考虑使用UCloud云计算旗下的Compshare的GPU算力云平台。他们提供高性价比的4090 GPU&#xff0c;按时收费每卡2.6元&#xff0c;月卡只需要1.7元每小时&…

Activity、Window、DecorView的关系

目录 一、Activity、Window、DecorView的层级关系如下图所示&#xff1a; 1、Activity 2、Window 3、DecorView 二、DecorView初始化相关源码 三、DecorView显示时机 前言&#xff1a; 不同的Android版本有差异&#xff0c;以下基于Android 11进行讲解。 一、Activi…

音乐发行平台无加密开源源码

适用于唱片公司&#xff0c;用于接收物料&#xff0c;下载物料功能&#xff1a;个人或机构认证&#xff0c;上传专辑和歌曲&#xff0c;版税结算环境要求php7.4Nginx 1、导入数据库 2、/inc/conn.php里填写数据库密码等后台路径/admin&#xff08;可自行修改任意入口名称&…

Meta 3D Gen:文生 3D 模型

是由 Meta 公布的一个利用 Meta AssetGen&#xff08;模型生成&#xff09;和 TextureGen&#xff08;贴图材质生成&#xff09;的组合 AI 系统&#xff0c;可以在分分钟内生成高质量 3D 模型和高分辨率贴图纹理。 视频演示的效果非常好&#xff0c;目前只有论文&#xff0c;期…

计算机网络--网络层

一、网络层的服务和功能 网络层主要为应用层提供端对端的数据传输服务 网络层接受运输层的报文段&#xff0c;添加自己的首部&#xff0c;形成网络层分组。分组是网络层的传输单元。网络层分组在各个站点的网络层之间传输&#xff0c;最终到达接收方的网络层。接收方网络层将运…

PLC_博图系列☞TP:生成脉冲

PLC_博图系列☞TP&#xff1a;生成脉冲 文章目录 PLC_博图系列☞TP&#xff1a;生成脉冲背景介绍TP&#xff1a; 生成脉冲说明参数脉冲时序图示例 关键字&#xff1a; PLC、 西门子、 博图、 Siemens 、 TP 背景介绍 这是一篇关于PLC编程的文章&#xff0c;特别是关于西门…

快速上手文心一言指令:解锁AI对话新纪元

快速上手文心一言指令 一、引言&#xff1a;文心一言的魅力所在二、准备工作&#xff1a;了解文心一言平台2.1 轻松注册&#xff0c;开启智能对话之旅2.2 深度探索&#xff0c;掌握界面布局奥秘2.2.1 输入框&#xff1a;智慧交流的起点2.2.2 回复区&#xff1a;即时反馈的窗口2…

Echarts-折线图

1.案例1 1.1代码 option {"tooltip": {"trigger": "axis","backgroundColor": "rgba(32, 33, 36,.7)","borderColor": "rgba(32, 33, 36,0.20)","borderWidth": 10,"textStyle"…

星辰资讯 | TiUP v1.16 发版,支持 PD 微服务

如果你对 TiDB 还不太了解&#xff0c;或者你对数据库安装部署的认知仍然停留在手动和脚本时代&#xff0c;那么&#xff0c;请先戳这里了解一下 TiUP 神器&#xff1a; 震惊&#xff01;数据库小白装国产数据库只需10分钟&#xff01; TiDB 7.x 源码编译之 TiUP 篇 TiUP&#…

基于改进高斯-拉普拉斯滤波器的一维时间序列平滑与降噪(MATLAB)

以图像处理为例&#xff0c;拉普拉斯算子是基于图像的二阶导数来找到边缘并搜索过零点&#xff0c;传统的拉普拉斯算子常产生双像素宽的边缘&#xff0c;对于较暗区域中的亮斑进行边缘检测时&#xff0c;拉普拉斯运算就会使其变得更亮。因此&#xff0c;与梯度算子一样&#xf…