一篇文章讲透排序算法之归并排序

news2024/11/18 19:56:18

0.前言

本篇文章将详细解释归并排序的原理,以及递归和非递归的代码原理。

一.概念

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

二.具体思想

根据上述所说,我们应当首先将序列不断分为子序列,也就是说将下面的数组分为左右两个部分,然后想办法让左数组有序,右数组有序,之后将这两个数组合并,即可让数组有序。而原数组的左右两部分又可以不断的分割为新的左右数组,那么我们就可以不断的将这个数组分割,直到左右数组内只有一个元素停止。如下图所示:

由于现在的左右数组都只有一个元素,那它们各自自然都是有序的,我们就可以将其合并,得到一个有序数组。并不断的进行这个操作,我们即可让原数组有序。以原数组的左子数组为例,我们可做出这些行为:

三.做法

现在我们已经理解了他的原理,那么我们应该怎么做呢?

方法1:递归

我们发现,它每次都是将序列分为左右两部分,然后每次都是分为两部分继续遍历,分到不可分割后,我们便不再进行分割,而是进行归并。

而我们二叉树的遍历也是一直分左右子树;而二叉树的后序遍历中则是先将左右都遍历完再进行打印的,我们会发现它们极其类似。

既然如此,我们就可以使用处理二叉树的后序遍历的递归思路来处理这个问题。

根据上述思路,我们可以写出如下代码:

void MergeSort(int* a, int begin,int end)
{   //递归的终止条件
    if(begin>=end)
    {
       return;
    }
	//分割
	int mid = (begin + end) / 2;
	MergeSort(a, begin, mid);
	MergeSort(a, mid+1, end);
	//合并
	//......
}

那么,我们下面的工作就是进行合并了。

我们发现,在合并的过程中,在原数组上操作会出现问题,因此我们需要开辟一块空间来保存合并后的数组,因此我们刚刚的函数体的参数列表则不可满足我们的需求。因此我们还应传入一个地址指向一块我们开辟的空间。

void MergeSort(int* a, int begin, int end, int* temp)
{
	//递归的终止条件
	if (begin >= end)
	{
		return;
	}
	//分割
	int mid = (begin + end) / 2;
	MergeSort(a, begin, mid, temp);
	MergeSort(a, mid+1,end, temp);
	//合并
}

我们写出的函数是想要拿来就可以直接用的,而不是还需要做一些准备工作才能用。

我们并不想每次开空间时都要先开辟一块空间,这要怎么办呢?

我们可以通过函数的回调来解决这个问题。 

void _MergeSort(int* a, int begin, int end, int* temp)
{
	//递归的终止条件
	if (begin >= end)
	{
		return;
	}
	//分割
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, temp);
	_MergeSort(a, mid+1,end, temp);
	//合并
    //......
}
void MergeSort(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	if (!temp)
	{
		perror("malloc fail!");
	}
	_MergeSort(a,0,n,temp);
}

现在我们就可以做到直接使用了,也不需要传递那么多乱七八糟的参数,只需要将数组的地址和长度传入即可。

那么现在我们来完成我们的归并过程。

我们拿上例中倒数第二趟的归并为例进行分析:

ps:此时两个小数组都已经有序

这里我们可以定义两个指针,分别指向两个小数组的第一个元素,然后我们比较指针指向的值,谁小就放到原数组的begin位置,之后再将其指针加1,之后再比较指针的值,直到一方遍历结束,再将另一方的元素拷贝进temp数组即可。过程如下:

过程1:

过程2:

我们下面重复这个操作即可完成对数组的排序。 

那么现在我们来完成代码部分:

void _MergeSort(int* a, int begin, int end, int* temp)
{
	//递归的终止条件
	if (begin >= end)
	{
		return;
	}
	//分割
	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, temp);
	_MergeSort(a, mid+1,end, temp);
	//合并
	int begin1 = begin, end1 = mid;//左区间
	int begin2 = mid + 1, end2 = end;//右区间
	int i = begin;//用来给temp赋值
	while (begin1<=end1&&begin2<=end2)//任何一个越界则结束
	{
		if (a[begin1] < a[begin2])
		{
			temp[i++] = a[begin1++];
		}
		if (a[begin2] < a[begin1])
		{
			temp[i++] = a[begin2++];
		}
	}
	//一个不越界,另外一个不越界
	// 下面的while循环必然会进入而且只会进入一个
	while (begin1 <= end1)
	{
		temp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		temp[i++] = a[begin2++];
	}
	//每次归并完成都要将数据拷贝一份回去
	memcpy(a+begin,temp+begin,sizeof(int)*(end-begin+1))
}
void MergeSort(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	if (!temp)
	{
		perror("malloc fail!");
	}
	_MergeSort(a,0,n.temp);
}

方法2:非递归

我们现在再来分析一下这一问题。

我们发现,我们会先两个两个分成一组进行排序。

然后再四个四个分成一组进行排序。

那么我们是否可以通过控制区间来完成这个排序呢?

首先,我们应我们确立一个gap,每次排序完一趟之后都将gap*2,直到gap超过数组的长度。

void MergeSort(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	int gap = 1;
	while (gap < n)
	{
        //归并代码
        //......
		gap *= 2;
	}
}

 之后,我们便可以控制单趟的排序了。

我们每趟都是将原数组分为很多对小数组进行比较的,每次比较完毕之后需要跳过一对小数组,直到比较到数组尾为止。

void MergeSort(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			//比较逻辑
			//......
		}
		gap *= 2;
	}
}

 比较的逻辑和我们递归中的是一样的。

需要我们注意的仅仅只是每一轮的开始处都等于i。

void MergeSortNonR(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	//开辟成功
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n; i += 2 * gap)
		{   //注意,每一轮的i都相当于递归方法中的begin
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
					temp[j++] = a[begin1++];
				else
					temp[j++] = a[begin2++];
			}
			while (begin1 <= end1)
				temp[j++] = a[begin1++];
			while (begin2 <= end2)
				temp[j++] = a[begin2++];
		}
		memcpy(a, temp, sizeof(int) * n);
		gap *= 2;
	}
}

现在我们基本完成了归并排序,最起码,长度为8的数组是能够完成的。

但是还有一个问题需要大家思考:

如果我们的数组长度是10的话,会出现什么样的情况呢?

如下所示:

这是为什么呢?

我们打印一下我们每次比较的区间来看一下

我们发现,出现了越界访问的情况。

通过上图分析,我们可以得到它们越界的情况(begin1不可能越界,因为被外循环限制

分别如下: 

  • end2越界
  • begin2,end2越界
  • end1,begin2,end2越界。

那么我们只要控制好这个边界,即可让这个程序很健壮。

那么我们如何控制这个边界情况呢?

 控制方法1:

当end1或begin2越界时,直接跳出循环,不进行归并,未归并的部分就会留存在原数组当中。

如果等到归并结束了之后再整体拷贝temp回原数组,就会覆盖掉没有归并的区域。

为了避免直接将temp中全部元素拷贝回原数组,保持原数组中未被归并部分的数据,

我们要每次归并都拷贝一次数据,而且只拷贝归并的部分。

如果将temp整体拷贝过去的话,上例中下标7后面的数据将会是乱码。

因此我们可以得到如下代码:

void MergeSortNonR(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		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)
			{
				end = n - 1;
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
					temp[j++] = a[begin1++];
				else
					temp[j++] = a[begin2++];
			}
			while (begin1 <= end1)
				temp[j++] = a[begin1++];
			while (begin2 <= end2)
				temp[j++] = a[begin2++];
			memcpy(a + i, temp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}
}
控制方法2

当end1越界时,我们直接将end1设置为数组最后一个元素为止,让begin2和end2为一个不存在的区间。

当begin2越界时,让begin2和end2设置为一个不存在的区间。

当end2越界时,将end2设置为数组最后一个元素。 

void MergeSortNonR(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	if (temp == NULL)
	{
		perror("malloc fail");
		exit(1);
	}
	//开辟成功
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n; i += 2 * gap)
		{   //注意,每一轮的i都相当于递归方法中的begin
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			if (end1 >= n)
			{
				end1 = n - 1;
				//[2,1]的区间就寄掉了。
				begin2 = 2;
				end2 = 1;
			}
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
					temp[j++] = a[begin1++];
				else
					temp[j++] = a[begin2++];
			}
			while (begin1 <= end1)
				temp[j++] = a[begin1++];
			while (begin2 <= end2)
				temp[j++] = a[begin2++];
		}
		memcpy(a, temp, sizeof(int) * n);//一趟归并一次
		gap *= 2;
	}
}

这样有个优点在于,每次原数组中的元素被会归并到每个数据都可以在归并后拷贝到temp数组中,因此我们就可以不像控制方法1那样归并一次拷贝一次了,我们归并一趟之后再拷贝即可。

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

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

相关文章

[消息队列 Kafka] Kafka 架构组件及其特性(二)Producer原理

这边整理下Kafka三大主要组件Producer原理。 目录 一、Producer发送消息源码流程 二、ACK应答机制和ISR机制 1&#xff09;ACK应答机制 2&#xff09;ISR机制 三、消息的幂等性 四、Kafka生产者事务 一、Producer发送消息源码流程 Producer发送消息流程如上图。主要是用…

【Python】使用Gradio作为机器学习web服务器

在机器学习领域&#xff0c;模型的展示和验证是一个重要的环节。传统的模型展示方式往往需要复杂的Web开发知识&#xff0c;这对于许多机器学习研究者或数据科学家来说可能是一个挑战。然而&#xff0c;Gradio的出现为我们提供了一个简单而强大的解决方案&#xff0c;让我们能够…

ffmpeg视频编码原理和实战-(2)视频帧的创建和编码packet压缩

源文件&#xff1a; #include <iostream> using namespace std; extern "C" { //指定函数是c语言函数&#xff0c;函数名不包含重载标注 //引用ffmpeg头文件 #include <libavcodec/avcodec.h> } //预处理指令导入库 #pragma comment(lib,"avcodec.…

【Week-R2】使用LSTM实现火灾预测(tf版本)

【Week-R2】使用LSTM实现火灾预测&#xff08;tf版本&#xff09; 一、 前期准备1.1 设置GPU1.2 导入数据1.3 数据可视化 二、数据预处理(构建数据集)2.1 设置x、y2.2 归一化2.3 划分数据集 三、模型创建、编译、训练、得到训练结果3.1 构建模型3.2 编译模型3.3 训练模型3.4 模…

虚拟机Ubuntu 22.04上搭建GitLab操作步骤

GitLab是仓库管理系统&#xff0c;使用Git作为代码管理工具。GitLab提供了多个版本&#xff0c;包括社区版(Community Edition)和企业版(Enterprise Edition)。实际应用场景中要求CPU最小4核、内存最小8GB&#xff0c;非虚拟环境。 以下是在虚拟机中安装社区版步骤&#xff1a;…

C++青少年简明教程:C++函数

C青少年简明教程&#xff1a;C函数 C函数是一段可重复使用的代码&#xff0c;用于执行特定的任务&#xff0c;可以提高代码的可读性和可维护性。函数可以接受参数&#xff08;输入&#xff09;并返回一个值&#xff08;输出&#xff09;&#xff0c;也可以没有参数和返回值。 …

应用层——HTTP协议(自己实现一个http协议)——客户端(浏览器)的请求做反序列化和请求分析,然后创建http向响应结构

应用层&#xff1a;之前我们写的创建套接字&#xff0c;发送数据&#xff0c;序列化反序列化这些都是在写应用层 我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应用层 之前的网络计算机是我们自定义的协议&#xff1a;传输的数据最终是什么样的结…

Redis缓存(笔记二:Redis常用五大数据类型)

目录 1、Redis中String字符串 1.1 常用命令解释&#xff1a; 1.2 原子性 1.3 具有原子性的常用命令 1.4 String数据结构 1、Redis中String字符串 概念 String 是 Redis 最基本的类型&#xff0c;可以理解成与 Memcached 一模一样的类型&#xff0c;一个 key对应一个 value…

Go微服务: 基于使用场景理解分布式之二阶段提交

概述 二阶段提交&#xff08;Two-Phase Commit&#xff0c;2PC&#xff09;是一种分布式事务协议&#xff0c;用于在分布式系统中确保多个参与者的操作具有原子性即所有参与者要么全部提交事务&#xff0c;要么全部回滚事务&#xff0c;以维持数据的一致性它分为两个阶段进行&…

php反序列化中的pop链

目录 一、什么是POP 二、成员属性赋值对象 例题&#xff1a; 方法一 方法二 三、魔术方法的触发规则 例题&#xff1a; 四、POC的编写 例题1&#xff1a; 例题2 [NISACTF 2022]babyserialize 今日总结&#xff1a; 一、什么是POP 在反序列化中&#xff0c;我们…

DexCap——斯坦福李飞飞团队泡茶机器人:更好数据收集系统的原理解析、源码剖析

前言 2023年7月&#xff0c;我司组建大模型项目开发团队&#xff0c;从最开始的论文审稿&#xff0c;演变成目前的两大赋能方向 大模型应用方面&#xff0c;以微调和RAG为代表 除了论文审稿微调之外&#xff0c;目前我司内部正在逐一开发论文翻译、论文对话、论文idea提炼、论…

RDMA (1)

RDMA是什么 Remote Direct Memory Access(RDMA)是用来给有高速需求的应用释放网络消耗的。 RDMA在网络的两个应用之间进行低延迟,高吞吐的内存对内存的直接数据通信。 InfiniBand需要部署独立的协议。 RoCE(RDMA over Converged Ethernet),也是由InfiniBand Trade Associat…

不要硬来!班组管理有“巧思”

班组管理&#xff0c;听起来似乎是一个充满“硬气”的词汇&#xff0c;让人联想到严肃、刻板的制度和规矩。然而&#xff0c;在实际操作中&#xff0c;我们却需要运用一些“巧思”&#xff0c;以柔克刚&#xff0c;让班组管理既有力度又不失温度。 在班组管理中&#xff0c;我们…

Istio_1.17.8安装

项目背景 按照istio官网的命令一路安装下来&#xff0c;安装好的istio版本为目前的最新版本&#xff0c;1.22.0。而我的k8s集群的版本并不支持istio_1.22的版本&#xff0c;导致ingress-gate网关安装不上&#xff0c;再仔细查看istio的发布文档&#xff0c;如果用istio_1.22版本…

Fatfs

STM32进阶笔记——FATFS文件系统&#xff08;上&#xff09;_stm32 fatfs-CSDN博客 STM32进阶笔记——FATFS文件系统&#xff08;下&#xff09;_stm32 文件系统怎样获取文件大小-CSDN博客 STM32——FATFS文件基础知识_stm32 fatfs-CSDN博客 021 - STM32学习笔记 - Fatfs文件…

Go select 语句使用场景

1. select介绍 select 是 Go 语言中的一种控制结构&#xff0c;用于在多个通信操作中选择一个可执行的操作。它可以协调多个 channel 的读写操作&#xff0c;使得我们能够在多个 channel 中进行非阻塞的数据传输、同步和控制。 基本语法&#xff1a; select {case communica…

纷享销客集成平台(iPaaS)的应用与实践

案例一 企业系统集成的产品级解决方案 概况 随着国家出台一系列鼓励LED照明产业发展与创新的规划和政策&#xff0c;以及国际市场全球演唱会、音乐会的活跃以及线上零售、商业地产等行业回暖&#xff0c;LED显示行业发展形势积极向好。深圳市艾比森光电股份有限公司&#xff…

第一周:计算机网络概述(上)

一、计算机网络基本概念 1、计算机网络通信技术计算机技术 计算机网络就是一种特殊的通信网络&#xff0c;其特殊之处就在于它的信源和信宿就是计算机。 2、什么是计算机网络 在计算机网络中&#xff0c;我们把这些计算机统称为“主机”&#xff08;上图中所有相连的电脑和服…

【动手学深度学习】softmax回归的简洁实现详情

目录 &#x1f30a;1. 研究目的 &#x1f30a;2. 研究准备 &#x1f30a;3. 研究内容 &#x1f30d;3.1 softmax回归的简洁实现 &#x1f30d;3.2 基础练习 &#x1f30a;4. 研究体会 &#x1f30a;1. 研究目的 理解softmax回归的原理和基本实现方式&#xff1b;学习如何…

开发人员必备的常用工具合集-lombok

Project Lombok 是一个 java 库&#xff0c;它会自动插入您的编辑器和构建工具&#xff0c;为您的 Java 增添趣味。 再也不用编写另一个 getter 或 equals 方法了&#xff0c;只需一个注释&#xff0c;您的类就拥有了一个功能齐全的构建器&#xff0c;自动化了您的日志记录变量…