当前位置: 首页 > news >正文

【排序】详细聊聊归并排序(含非递归)

目录

归并排序的基本思想:

递归算法:

递归算法的思路分析:

开辟数组的函数:

递归的函数:

非递归算法:

非递归的思路分析:

边界问题:

时间复杂度和空间复杂度分析:


归并排序的基本思想:

归并排序所采用的思想是将大问题分为小问题来处理,即我们常说的分治。

如图,要排序一个数组,我们就把这个数组往小的区间拆分,比如要排序数组,先排序左边数组,再排序右边数组,被分开的两个数组可以继续按照这种方式,继续分成更小的区间,直到每个区间只有一个数字的时候,再通过比较 相邻区间来插入到数组中,当然,这里插入的数组我们需要额外开辟。

归并排序的逻辑我们理清楚后,有两种方式来实现:递归和非递归,我们分别来介绍一下

递归算法:

递归算法的思路分析:

显然归并排序将大问题分治为小问题的思想是非常显然也是很适合用递归去解决的

写好递归逻辑的代码,由于我们需要开辟数组,因此我们将递归代码单独封装成另一个函数,在开辟数组的函数里调用就可以:

开辟数组的函数:

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

	_MergeSort(a,0,n-1,tmp);

	free(tmp);
	tmp = NULL;
}

递归的函数:

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);

	//归并[begin,mid] [mid+1,end]
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
    //每个区间的i都是不同的,因此i要从begin开始
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}
    //如果某个区间还剩下数据,直接插入到tmp的后面
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

	memcpy(a+begin,tmp + begin,sizeof(int)*(end - begin + 1));
}

非递归算法:

非递归的思路分析:

归并排序的递归算法非常简单且容易理解,我们主要来看非递归算法:

非递归算法本质也是分治的思想,只不过在非递归算法这里,我们直接从一个一个的数据开始排序

第一次排序范围:0~0   1~1   2~2   3~3   4~4   5~5   6~6   7~7(每组一个)

第二次排序范围:0~1   2~3   3~4   4~5   5~6   6~7(每组两个)

第三次排序范围:0~3   4~7(每组四个)

int rangeN = 1;
	while (rangeN < n)
	{
		for (int i = 0;i<n;i+=2*rangeN)
		{
			int begin1 = i, end1 = i + rangeN - 1; 
			int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;

			int j = i;
			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,tmp,sizeof(int)*n);
		rangeN *= 2;
	}

边界问题:

弄清楚非递归是如何处理数据的,我们还需要考虑一个问题:边界问题

 上述四个变量中,有三种越界的情况:end1越界,begin2越界,end2越界

针对上述问题,我们要修改对应的边界,防止越界:

void MergeSortNONR(int* a,int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		exit(-1);
	}
	int rangeN = 1;
	while (rangeN < n)
	{
		for (int i = 0;i<n;i+=2*rangeN)
		{
			int begin1 = i, end1 = i + rangeN - 1; 
			int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;

			int j = i;
			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 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])
				{
					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,tmp,sizeof(int)*n);

		rangeN *= 2;
	}
}

时间复杂度和空间复杂度分析:

时间复杂度:

归并排序的时间复杂度比较容易计算,由于是树的结构,时间复杂度是标准的O(nlogn)

空间复杂度:

由于归并排序需要开辟额外数组,所以空间复杂度是O(N),这也是归并排序的缺点


至此我们就讲解完成了递归方式和非递归方式实现归并排序,如果对你有所收获,还请点赞关注,我们下次再见

相关文章:

  • kafka单条消息过大导致线上OOM,运维连夜跑路了!
  • ValidateCode验证码的使用详解(初学看完都会用)
  • 肝了一周总结的SpringBoot常用注解大全,一目了然!
  • 无线电信号密钥WiFi完整版学习教程
  • Linux----paste命令使用详解
  • 【LSTM时序预测】基于灰狼算法优化长短时记忆网络GWO-LSTM实现风电功率预测附Matlab代码
  • 语音识别芯片LD3320介绍
  • Java的List之坑系列--Collections#unmodifiableList仍然可变
  • 我的一周年创作纪念日
  • idea中推送本地仓库和远程仓库后代码回退
  • Ultra-high Resolution Image Segmentation via Locality-aware Context Fusion
  • Windows取证——数据恢复(Fat32文件系统和NTFS文件系统)
  • 记一次 .NET 某安全生产信息系统 CPU爆高分析
  • 阻塞队列的使用
  • TypeScript 对象的简单使用和操作包含number对象等
  • AHK 中 = 和 == 等比较运算符的用法
  • C++入门教程(10):for 语句
  • Linux快速复制或删除大量小文件
  • linux学习笔记
  • pdf文件如何在线转换为jpg图片
  • react-native 安卓真机环境搭建
  • Web设计流程优化:网页效果图设计新思路
  • 关于字符编码你应该知道的事情
  • 近期前端发展计划
  • 小而合理的前端理论:rscss和rsjs
  • 在 Chrome DevTools 中调试 JavaScript 入门
  • ​sqlite3 --- SQLite 数据库 DB-API 2.0 接口模块​
  • ​什么是bug?bug的源头在哪里?
  • # 计算机视觉入门
  • #Linux(Source Insight安装及工程建立)
  • #QT(智能家居界面-界面切换)
  • $GOPATH/go.mod exists but should not goland
  • (3)(3.2) MAVLink2数据包签名(安全)
  • (4)(4.6) Triducer
  • (二)pulsar安装在独立的docker中,python测试
  • (附源码)ssm跨平台教学系统 毕业设计 280843
  • (黑马出品_高级篇_01)SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式
  • (算法)前K大的和
  • (转) ns2/nam与nam实现相关的文件
  • (转)可以带来幸福的一本书
  • (转)母版页和相对路径
  • . Flume面试题
  • ./configure、make、make install 命令
  • .Net 垃圾回收机制原理(二)
  • .net(C#)中String.Format如何使用
  • .NET/C# 推荐一个我设计的缓存类型(适合缓存反射等耗性能的操作,附用法)
  • .NET面试题解析(11)-SQL语言基础及数据库基本原理
  • .NET与java的MVC模式(2):struts2核心工作流程与原理
  • .NET中winform传递参数至Url并获得返回值或文件
  • @CacheInvalidate(name = “xxx“, key = “#results.![a+b]“,multi = true)是什么意思
  • @KafkaListener注解详解(一)| 常用参数详解
  • [asp.net core]project.json(2)
  • [CF494C]Helping People
  • [HTML]Web前端开发技术6(HTML5、CSS3、JavaScript )DIV与SPAN,盒模型,Overflow——喵喵画网页
  • [java进阶]——方法引用改写Lambda表达式