排序算法:归并排序(递归和非递归)

news2024/11/29 10:37:21

朋友们、伙计们,我们又见面了,本期来给大家解读一下有关排序算法的相关知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

目录

1.归并排序

1.1递归版本

代码演示:

1.2非递归版本 

代码演示:

测试排序:

改正代码1:

测试排序:

改正代码2:

1.3递归版本的优化

代码演示:

2.归并排序特性


1.归并排序

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

 

归并排序也分为递归和非递归版本,下面我们就来逐步学习: 

1.1递归版本

从上面的图中我们可以看出归并排序是分为两个阶段分解合并,那么转化为代码的思想就是先划分小区间,然后将区间中最小的单独拿出来在另外的一个数组中进行尾插,等到最后一组数据排完之后,再将尾插排序完的整个数组再拷贝至原数组,这样子就完成了整个数组的排序。

那么通过这个过程可以发现:归并排序是需要单独开一个数组的,所以它的

空间复杂度是O(N),另外归并排序是先划分小区间再进行排序,那么就和二叉树中的后序遍历逻辑类似,先将整个数据一分为二,使得左区间和右区间有序,然后再将左右两个区间进行排序,那么整个数据就有序了,所以需要让左右区间再有序,也需要将做右区间各划分为两个区间,并且让它们的左右区间再有序,以此类推,直到区间内只剩一个数据就不需要再划分了,然后取两个区间小的值尾插到单独的一个数组,最后再次整体拷贝至原数组。

在递归时要注意几个问题:

1. 在递归的时候需要保存两个区间的起始和终止位置,以便访问。

2. 当一个区间已经尾插完毕,那么直接将另外一个区间的数据依次尾插。

3. 两个区间的数据都尾插完毕至tmp数组时,需要将tmp数组的数据再次拷贝至原数组。

4. 在拷贝到原数组时需要注意尾插的哪一个区间就拷贝哪一个区间。 

代码演示:

void _MergerSort(int* a, int begin, int end, int* tmp)
{
	//递归截止条件
	if (begin == end)
		return;

	//划分区间
	int mid = (begin + end) / 2;
	//[begin,mid] [mid+1,end]

	//递归左右区间
	_MergerSort(a, begin, mid, tmp);
	_MergerSort(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));
}

//归并排序
void MergerSort(int* a, int n)
{
	//先创建一个数组
	int* tmp = (int*)malloc(sizeof(int) * n);

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

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

1.2非递归版本 

由于递归代码在数据太多时可能会因为递归太深出现问题,所以我们也需要写出它们所对应的非递归版本的排序代码:非递归版本的归并排序直接可以使用循环来完成,但是坑点非常多,接下来我们就来慢慢排查。

我们可以先一个数据为一组,然后两两进行排序,然后将排序完的整体结果再重新拷贝至原数组,这样子就完成了一次排序,然后再将排序完的结果两个数据为一组,然后两两排序,然后将排序完的数据再拷贝至原数组,这样子就完成了第二次的排序,然后将数据四个分为一组,两两进行排序,再将排序完的数据拷贝至原数组,直到每组的数据超过或者等于数据的总长度即不需要在进行排序。

首先我们先创建一个开辟一个数组,然后设置一个gap用来分组,然后记录两个区间,对两个区间的数进行比较,小的尾插,再将剩余数据继续尾插,然后完成了一趟排序,再将排完序的数据再拷贝至原数组,再将gap * 2,继续完成剩余的排序,直到划分组的gap大于等于数据总长度即可完成全部的排序:

代码演示:

//归并排序
//非递归
void MergerSortNonR(int* a, int n)
{
	//创建数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("tmp");
		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;


			//取两个区间小的值尾插
			//一个区间尾插完毕另一个区间直接尾插即可
			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);
		//更新gap
		gap *= 2;
	}

	//释放
	free(tmp);
}
测试排序:
void PrintArry(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void TestMergerSortNonR()
{
	int a[] = { 10,6,7,1,3,9,4,2 };
	PrintArry(a, sizeof(a) / sizeof(int));
	MergerSortNonR(a, sizeof(a) / sizeof(int));
	PrintArry(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestMergerSortNonR();
	return 0;
}

可以看到排序完成,而且还完成的不错,那么我们再来几组数据进行测试:

在这里我们使用的是8个数据,那如果我们使用9个或者10个数据呢?

void PrintArry(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void TestMergerSortNonR()
{
	int a[] = { 10,6,7,1,3,9,4,2,8,5 };
	PrintArry(a, sizeof(a) / sizeof(int));
	MergerSortNonR(a, sizeof(a) / sizeof(int));
	PrintArry(a, sizeof(a) / sizeof(int));
}


int main()
{
	TestMergerSortNonR();
	return 0;
}

可以看到数据发生错误,那么到底是为什么呢?我们可以来一起观察一下:

随着gap的2倍递增,那么会发生数据区间越界的问题,因为当数据是10个的时候,gap会递增到8,因此在访问数据的时候会发生越界,我们也可以观察一下这个越界的现象:

可以将数据访问的区间打印出来:

那么该怎么解决这个问题呢?

1. 首先不能排完一次序然后将数据整体拷贝,需要排完一组,拷贝一组。

2. 其次当发生越界中的第一、二种时可以直接break。

3. 当发生越界中的第三种时可以将边界进行修正。

改正代码1:

//归并排序
//非递归
void MergerSortNonR(int* a, int n)
{
	//创建数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("tmp");
		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;
			
			//end1和begin2越界直接跳出
			if (end1 >= n || begin2 >= n)
			{
				break;
			}

			//end2越界可以进行修正
			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);
}
测试排序:
void PrintArry(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

void TestMergerSortNonR()
{
	int a[] = { 10,6,7,1,3,9,4,2,8,5 };
	PrintArry(a, sizeof(a) / sizeof(int));
	MergerSortNonR(a, sizeof(a) / sizeof(int));
	PrintArry(a, sizeof(a) / sizeof(int));
	printf("\n");

	int a2[] = { 10,6,7,1,3,9,4,2 };
	PrintArry(a2, sizeof(a2) / sizeof(int));
	MergerSortNonR(a2, sizeof(a2) / sizeof(int));
	PrintArry(a2, sizeof(a2) / sizeof(int));
	printf("\n");

	int a3[] = { 10,6,7,1,3,9,4,2,8 };
	PrintArry(a3, sizeof(a3) / sizeof(int));
	MergerSortNonR(a3, sizeof(a3) / sizeof(int));
	PrintArry(a3, sizeof(a3) / sizeof(int));
	printf("\n");

}


int main()
{
	TestMergerSortNonR();
	return 0;
}

改正过后就完全的解决了越界的问题,那么这种改进方法是归并一组,拷贝一组。

我们也可以将全部越界的区间进行修正,然后排序完一次将整个数据拷贝。

改正代码2:

将越界区间全部修正也是可以达到改进的目的,我们就以归并数据的逻辑为基础,然后修改区间,因此需要将越界的区间改为不存在的区间即可:

//归并排序
//非递归
//改进代码2:
void MergerSortNonR(int* a, int n)
{
	//创建数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	//划分组数
	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)
			{
				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);
		gap *= 2;
	}

	//释放
	free(tmp);
}

 

1.3递归版本的优化

 当数据量非常多的时候使用递归版本可以进行一个优化,当递归到小区间的时候,我们可以采用插入排序来进行优化,这个优化只限于递归版本,在进行小区间中的插入排序时需要注意在前面的步骤递归到了哪个区间就使用插入排序排序哪个区间,所以在进行插入排序的时候需要注意排序的区间。

代码演示:

void _MergerSort(int* a, int begin, int end, int* tmp)
{
	//递归截止条件
	if (begin == end)
		return;

	小区间优化
	//区间过小时直接使用插入排序,减少递归损耗
	if (end - begin + 1 < 10)
	{
		//         注意排序的区间
		InsertSort(a + begin, end - begin + 1); 
		return;
	}

	//划分区间
	int mid = (begin + end) / 2;
	//[begin,mid] [mid+1,end]

	//递归左右区间
	_MergerSort(a, begin, mid, tmp);
	_MergerSort(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));
}

//归并排序
void MergerSort(int* a, int n)
{
	//先创建一个数组
	int* tmp = (int*)malloc(sizeof(int) * n);

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

	//释放
	free(tmp);
}

2.归并排序特性

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

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持! 

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

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

相关文章

关于使用API接口获取商品数据的那些事(使用API接口获取商品数据的步骤和注意事项。)

随着电商行业的不断发展&#xff0c;越来越多的企业和个人需要获取各大电商平台上的商品数据。而最常用的方法是使用API接口获取商品数据。本文将为您介绍使用API接口获取商品数据的步骤和注意事项。 一、选择API接口 首先需要了解各大电商平台提供的API接口&#xff0c;目前…

算法通关村第14关【黄金】| 数据流的中位数

思路&#xff1a;使用一个小根堆一个大根堆来找中位数 小根堆保存较大的一半数字&#xff0c;大根堆保存较小的一半数字 奇数queMin的队头即为中位数&#xff0c;偶数queMin和queMax队头相加/2为中位数 初始状态&#xff1a; queMin: [] queMax: [] 添加数字 1&#xff1a; …

【Java 基础篇】Java 进程详解:从基础到实践

Java 是一种广泛应用于各种类型的软件开发的编程语言&#xff0c;而与 Java 紧密相关的一个概念就是进程。本篇博客将从基础开始&#xff0c;详细介绍 Java 进程的概念、创建、管理以及一些实际应用场景。无论您是初学者还是有一定经验的开发者&#xff0c;都能从本文中获取有关…

如何高效批量查询快递单号,提高工作效率?

在日常生活中&#xff0c;快递单号的查询是一项常规任务。过去&#xff0c;这项任务需要通过人工一个一个地在快递平台上查询&#xff0c;既耗时又费力。然而&#xff0c;随着科技的发展&#xff0c;我们有了更多的工具可以帮助我们高效地完成这项任务。本文将介绍如何使用固乔…

【List篇】LinkedList 详解

目录 成员变量属性构造方法add(), 插入节点方法remove(), 删除元素方法set(), 修改节点元素方法get(), 取元素方法ArrayList 与 LinkedList的区别Java中的LinkedList是一种实现了List接口的 双向链表数据结构。链表是由一系列 节点(Node)组成的,每个节点包含了指向 上一个…

Java“牵手”1688商品评论数据采集+1688商品评价接口,1688商品追评数据接口,行业商品质检接口,1688API接口申请指南

1688商品评论平台是阿里巴巴集团旗下的一个在线服务市场平台&#xff0c;为卖家提供商品评价服务。平台上有多种评价工具和数据支持&#xff0c;可以帮助卖家更好地了解商品的质量和特点&#xff0c;从而做出更明智的采购决策。 1688商品评论平台支持多种评价方式&#xff0c;…

R语言画多变量间的两两相关性图

语言代码&#xff1a; setwd("D:/Desktop/0000/R") #更改路径df<-read.csv("kaggle/Seed_Data.csv") head(df) df$target<-factor(df$target) # 因为目标是数字&#xff0c;所以加他&#xff0c;不加会报错 cols<-c("steelblue","…

《动手学深度学习 Pytorch版》 7.1 深度卷积神经网络(LeNet)

7.1.1 学习表征 深度卷积神经网络的突破出现在2012年。突破可归因于以下两个关键因素&#xff1a; 缺少的成分&#xff1a;数据 数据集紧缺的情况在 2010 年前后兴起的大数据浪潮中得到改善。ImageNet 挑战赛中&#xff0c;ImageNet数据集由斯坦福大学教授李飞飞小组的研究人…

OpenCV中的HoughLines函数和HoughLinesP函数到底有什么区别?

一、简述 基于OpenCV进行直线检测可以使用HoughLines和HoughLinesP函数完成的。这两个函数之间的唯一区别在于,第一个函数使用标准霍夫变换,第二个函数使用概率霍夫变换(因此名称为 P)。概率版本之所以如此,是因为它仅分析点的子集并估计这些点都属于同一条线的概率。此实…

威胁的数量、复杂程度和扩散程度不断上升

Integrity360 宣布了针对所面临的网络安全威胁、数量以及事件响应挑战的独立研究结果。 数据盗窃、网络钓鱼、勒索软件和 APT 是最令人担忧的问题 这项调查于 2023 年 8 月 9 日至 14 日期间对 205 名 IT 安全决策者进行了调查&#xff0c;强调了他们的主要网络安全威胁和担忧…

评价指标分类

声明 本文是学习GB-T 42874-2023 城市公共设施服务 城市家具 系统建设实施评价规范. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本文件确立了城市家具系统建设实施的评价原则、评价流程&#xff0c;给出了评价指标&#xff0c;描述了 方…

nodejs定时任务

项目需求&#xff1a; 每5秒执行一次&#xff0c;多个定时任务错开&#xff0c;即cron表达式中斜杆前带数字&#xff0c;例如 ‘1/5 * * * * *’定时任务准时&#xff0c;延误低 搜索了nodejs的定时任务&#xff0c;其实不多&#xff0c;找到了以下三个常用的&#xff1a; n…

无涯教程-JavaScript - BETA.INV函数

描述 BETA.INV函数返回beta累积概率密度函数(BETA.DIST)的反函数。如果概率 BETA.DIST(x ... TRUE),则BETA.INV(概率...) x。 在预期的完成时间和可变性的情况下,可以在项目计划中使用beta分布来建模可能的完成时间。 语法 BETA.INV (probability,alpha,beta,[A],[B])争论 …

Linux中使用selenium截图的文字变为方框的解决方案

一、前言 最近在Linux中使用selenium截图时&#xff0c;发现文字都变为了方框&#xff1a; 虽然不影响selenium的使用&#xff0c;但有点影响调试&#xff0c;也不好看&#xff0c;后面发现是因为Linux缺少中文字体的缘故&#xff0c;需要安装中文字体就能解决。 二、安装中文…

安卓Android_手机安装burp的https_CA证书

安卓Android_手机安装burp的https_CA证书 文章目录 安卓Android_手机安装burp的https_CA证书1 打卡电脑wif热点&#xff0c;手机连上电脑的热点2 burp点击 --》 Proxy settings3 点击add ---》新增代理地址和端口4 设置好-展示5 手机连上电脑的wifi热点6 点击查看ip地址与burp …

这些英国学校接受ChatGPT帮助写作

自从ChatGPT展现了其高超的AI技术&#xff0c;全球年轻人纷纷对其表示喜爱&#xff0c;尤其是学生们&#xff0c;将其视为一个优化版的网络搜索引擎&#xff0c;可以用来提高学习效率。 ChatGPT具有多样化的功能&#xff0c;可以节省研究复杂文献的时间&#xff0c;编写简单的…

stm32学习-芯片系列/选型

【03】STM32HAL库开发-初识STM32 | STM概念、芯片分类、命名规则、选型 | STM32原理图设计、看数据手册、最小系统的组成 、STM32IO分配_小浪宝宝的博客-CSDN博客  STM32&#xff1a;ST是意法半导体&#xff0c;M是MCU/MPU&#xff0c;32是32位。  ST累计推出了&#xff1a…

AIGC生成式代码——Code Llama 简介、部署、测试、应用、本地化

导读: 本文介绍了CodeLlama的简介、本地化部署、测试和应用实战方案,帮助学习大语言模型的同学们更好地应用CodeLlama。我们详细讲解了如何将CodeLlama部署到实际应用场景中,并通过实例演示了如何使用CodeLlama进行代码生成和优化。最后,总结了CodeLlama的应用实战经验和注…

9.4.2servlet基础2

一.SmartTomcat 1.第一次使用需要进行配置. 二.异常处理 1.404:浏览器访问的资源,在服务器上不存在. a.检查请求的路径和服务器配置的是否一致(大小写,空格,标点符号). b. 确认webapp是否被正确加载(检查web.xml没有/目录错误/内容错误/名字拼写错误)(多多关注日志信息). 2…

typeof的作用

typeof 是 JavaScript 中的一种运算符&#xff0c;用于获取给定值的数据类型。 它的作用是返回一个字符串&#xff0c;表示目标值的数据类型。通过使用 typeof 运算符&#xff0c;我们可以在运行时确定一个值的类型&#xff0c;从而进行相应的处理或逻辑判断。 常见的数据类型…