<数据结构>NO11.归并排序|递归|非递归|优化

这篇具有很好参考价值的文章主要介绍了<数据结构>NO11.归并排序|递归|非递归|优化。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法

归并排序

递归写法

思路:
如果给出两个有序数组,我们很容易可以将它们合并为一个有序数组。因此当给出一个无序数组时,我们先将它们均分为两组有序数组,在将这两组有序数组合并为一个有序数组;而将原数组分成2组 有序数组的思路又是归并排序

递归的结束条件是什么?
当递归区间只有一个元素时结束递归

使用归并排序排3 5 7 6 9 2 10 8详细过程如下:

<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法

<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法

代码

void _MergeSort(int* arr, int* tmp, int begin, int end)
{
	//递归终止条件--区间只有一个数
	if (begin == end)
		return;
	//将数据均分未2组
	int mid = (begin + end) >> 1;
	int i = begin;
	//使两组均有序
	_MergeSort(arr, tmp, begin, mid);
	_MergeSort(arr, tmp, mid + 1, end);
	//有序数组归并---归并到tmp数组中
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] < arr[begin2])
			tmp[i++] = arr[begin1++];
		else
			tmp[i++] = arr[begin2++];
	}
	while (begin1 <= end1)	tmp[i++] = arr[begin1++];
	while (begin2 <= end2)	tmp[i++] = arr[begin2++];
	//数据拷贝回元素组中
	memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
//归并排序
//时间复杂度O(nlogn)
//空间复杂度O(n+logn)
void MergeSort(int* arr, int sz)
{
	int* tmp = (int*)malloc(sizeof(int) * sz);//tmp作为临时数组存放有序数组合并后的数据
	_MergeSort(arr, tmp, 0, sz - 1);		  //因为要借助临时数组所以构造子函数
	free(tmp);
}

注意:

  1. 由于mid向上取整,所以不存在递归区间不存在的情况,结束条件只有区间只有一个值

  2. 每次拷贝时是从arr+begin处开始拷贝


非递归写法

归并排序的非递归写法不能和快速排序一样通过栈实现,如果使用栈存归并的区间,那么每次取出区间归并时栈顶元素出栈。这就导致了栈中不在存放有关该区间的任何信息,但是归并排序要求对区间的值进行保存,所以不能使用栈来实现

思路:
对数组依次进行一一归并、两两归并、四四归并,直至数组最终有序。

<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法

代码

//归并排序非递归
void MergeSortNonR(int* arr, int sz)
{
	int* tmp = (int*)malloc(sizeof(int) * sz);//临时数组用来存放分组排序的结果
	assert(tmp);
	int gap = 1;//从间隔为1开始归并
	while (gap < sz)
	{
		int j = 0;
		for (int i = 0; i < sz; i += 2 * gap)//以gap为间隔归并完一组后归并下一组
		{
			//有序数组归并---归并到tmp数组中
			int begin1 = i, end1 = begin1 + gap - 1;
			int begin2 = end1 + 1, end2 = begin2 + gap - 1;
            //归并区间
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (arr[begin1] < arr[begin2])
					tmp[j++] = arr[begin1++];
				else
					tmp[j++] = arr[begin2++];
			}
			while (begin1 <= end1)	tmp[j++] = arr[begin1++];
			while (begin2 <= end2)	tmp[j++] = arr[begin2++];
		}
		//拷贝回元素组中
		memcpy(arr, tmp, sizeof(int) * sz);
		gap *= 2;
	}  

运行结果
<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法

看似没有问题,但是当我们在原数组基础上添加1个数据再排序时程序会崩溃<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法

我们走读代码看看哪里出了问题
<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法

gap=2,i = 8时,begin1 = 8, end1 = 9,此时在访问arr[end1]就会越界。同理,如果数组元素是10个,那么当gap=4,i = 8时,begin1 = 8, end1 = 11,此时访问arr[end1]同样会越界,所以只有当数组元素为2的幂次方时,上述排序才正确。因此我们需要对上述代码进行修改


修改:数组越界一共有3种情况,分别是end1越界,begin1越界,end2越界
<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法

修正方案1.归并一段拷贝一段

上面已经提过了当数据个数为9个时,两两归并时会访问arr[9], 并且将该值拷贝到不属于我们创建的堆空间中。造成了越界访问
为了避免将原数组外的空间拷贝我们选择归并一段拷贝一段,只拷贝我们当趟已经归并的区间.
如果end1或者begin2越界,直接break不归并这段区间也就拷贝,剩余的数据交给gap增大的下一次归并;如果遇见end2越界,则修正end2为sz-1,归并区间[begin1,end1]和[begin2, end2]
<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法

代码

//归并排序非递归(归并一趟拷贝一趟)
void MergeSortNonR(int* arr, int sz)
{
	int* tmp = (int*)malloc(sizeof(int) * sz);//临时数组用来存放分组排序的结果
	assert(tmp);
	int gap = 1;//从间隔为1开始归并
	while (gap < sz)
	{
		int j = 0;
		for (int i = 0; i < sz; i += 2 * gap)
		{
			//有序数组归并---归并到tmp数组中
			int begin1 = i, end1 = begin1 + gap - 1;
			int begin2 = end1 + 1, end2 = begin2 + gap - 1;
			//end1越界或者begin2越界跳出剩下为归并的数据交给下一次处理
			if (end1 >= sz || begin2 >= sz)
			{
				break;
			}
			//end2越界,修正end2
			if (begin2 < sz && end2 >= sz)
			{
				end2 = sz - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (arr[begin1] < arr[begin2])
					tmp[j++] = arr[begin1++];
				else
					tmp[j++] = arr[begin2++];
			}
			while (begin1 <= end1)	tmp[j++] = arr[begin1++];
			while (begin2 <= end2)	tmp[j++] = arr[begin2++];
			//归并一段拷贝一段
			memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		//拷贝回元素组中
		//memcpy(arr, tmp, sizeof(int) * sz);
		gap *= 2;
	}  

修正方案2.修正区间

方案1是归并一段拷贝一段,越界的那一部分区间不进行处理,让下一趟归并处理上一趟违为归并的数据。
方案2是先将越界的那一部分区间进行修正(修正的区间可能合法也可能不合法),在对区间进行以gap为间隔的归并,每次归并完整个数组后再拷贝(方案1归并完一个区间就拷贝)。

代码

//归并排序非递归(修正区间)
void MergeSortNonR(int* arr, int sz)
{
	int* tmp = (int*)malloc(sizeof(int) * sz);//临时数组用来存放分组排序的结果
	assert(tmp);
	int gap = 1;//从间隔为1开始归并
	while (gap < sz)
	{
		int j = 0;
		for (int i = 0; i < sz; i += 2 * gap)
		{
			//有序数组归并---归并到tmp数组中
			int begin1 = i, end1 = begin1 + gap - 1;
			int begin2 = end1 + 1, end2 = begin2 + gap - 1;
			//end1越界
			if (end1 >= sz)
			{
				//end1修正为边界
				end1 = sz - 1;
				//[begin2, end2]修正为不存在的区间
				begin2 = sz;
				end2 = sz - 1;
			}
			else if (begin2 >= sz)
			{
				//修正为不存在的区间
				begin2 = sz;
				end2 = sz - 1;
			}
			else if (end2 >= sz)
			{
				end2 = sz - 1;
			}
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (arr[begin1] < arr[begin2])
					tmp[j++] = arr[begin1++];
				else
					tmp[j++] = arr[begin2++];
			}
			while (begin1 <= end1)	tmp[j++] = arr[begin1++];
			while (begin2 <= end2)	tmp[j++] = arr[begin2++];
			
		}
		//拷贝回元素组中
		memcpy(arr, tmp, sizeof(int) * sz);
		gap *= 2;
	}  
	
	free(tmp);
}

算法优化

假设我们排100000个数据,归并排序每次排序都会进行递归调用,每一个区间都会递归调用2次,因此当递归区间长度减小到某一个数时,该递归区间在进行递归调用时递归的次数就会非常多<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法

总共递归 2 h − 1 次,最后一层递归次数占了全部的 % 50 , 倒数第二层的递归调用占了全部的 % 25 , , 倒数第三层递归调用占了全部的 % 12.5 , 因此最后三层占了所有递归调用次数的 % 87.5 总共递归2^{h}-1次,最后一层递归次数占了全部的\%50,倒数第二层的递归调用占了全部的\%25,,倒数第三层递归调用占了全部的\%12.5,因此最后三层占了所有递归调用次数的\%87.5 总共递归2h1次,最后一层递归次数占了全部的%50,倒数第二层的递归调用占了全部的%25,,倒数第三层递归调用占了全部的%12.5,因此最后三层占了所有递归调用次数的%87.5

当递归区间元素个数为10时,还会接着递归3层,因此我们可以进行小区间优化当递归区间长度小于等于10时,直接进行插入排序,这样可以大大减少递归调用次数。

优化代码

void _MergeSort(int* arr, int* tmp, int begin, int end)
{
	//递归终止条件--区间只有一个数
	if (begin == end)
		return;
	//小区间优化
	if (end - begin + 1 <= 10)
	{
		InsertSort(arr + begin, end - begin + 1);
		return;
	}
	//将数据均分未2组
	int mid = (begin + end) >> 1;
	int i = begin;
	//使两组均有序
	_MergeSort(arr, tmp, begin, mid);
	_MergeSort(arr, tmp, mid + 1, end);
	//有序数组归并---归并到tmp数组中
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] < arr[begin2])
			tmp[i++] = arr[begin1++];
		else
			tmp[i++] = arr[begin2++];
	}
	while (begin1 <= end1)	tmp[i++] = arr[begin1++];
	while (begin2 <= end2)	tmp[i++] = arr[begin2++];
	//拷贝回元素组中
	memcpy(arr + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
//归并排序
void MergeSort(int* arr, int sz)
{
	int* tmp = (int*)malloc(sizeof(int) * sz);//tmp作为临时数组存放有序数组合并后的数据
	_MergeSort(arr, tmp, 0, sz - 1);		  //因为要借助临时数组所以构造子函数
	free(tmp);
}

优化结果
<数据结构>NO11.归并排序|递归|非递归|优化,数据结构与算法,数据结构,算法,排序算法


算法分析

归并排序 时间复杂度 空间复杂度
递归 O ( n l o g n ) O(nlogn) O(nlogn) O ( n ) O(n) O(n)
非递归 O ( n l o g n ) O(nlogn) O(nlogn) O ( n ) O(n) O(n)

归并排序的应用

外排序和内排序

对内存中的数据进行排序称为内排序,对外存储器上的数据进行排序称为外排序。
现代计算机的内存通常在 8 − 16 G B 8-16GB 816GB之间可以存储的整数在 21 亿 − 42 亿 21亿-42亿 21亿42亿之间,如果我们要排 200 亿 200亿 200亿的数据量怎么处理?

  1. 将数据放在文件中,将大文件分为n份小文件使每个小文件存储的数据量可以放在内存中排序
  2. 对将每个小文件的数据放在内存中进行排序(归并、快排),使得每个小文件有序
  3. 对n个小文件外使用归并排序进行外排序

注意:
外排序只能使用归并排序,因此文件数据不能通过下标访问随机读取,文件指针习惯顺序读写,而归并排序不需要下标访问文章来源地址https://www.toymoban.com/news/detail-586707.html

到了这里,关于<数据结构>NO11.归并排序|递归|非递归|优化的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 数据结构排序——详细讲解归并排序(c语言实现递归及非递归)

    上次是快排和冒泡:数据结构排序——详解快排及其优化和冒泡排序(c语言实现、附有图片与动图示意) 今天为大家带来归并排序 归并排序是一种分治算法,它将序列分成两个子序列,分别对 子序列进行排序 ,然后将排序好的子序列 合并起来 。这个过程可以 递归 地进行,

    2024年01月22日
    浏览(50)
  • [数据结构 -- 手撕排序算法第七篇] 递归实现归并排序

    目录 1、归并的思想 2、归并排序的思想 2.1 基本思想 2.2 图解分析 3、归并排序递归版本代码实现 3.1 代码解析 3.2 注意事项 3.2.1错误划分:[begin, mid-1],[mid, end] 3.2.2 正确划分:[begin, mid], [mid+1, end] 4、归并排序的测试 5、时间复杂度、空间复杂度分析 5.1 时间复杂度 5.2 空间复杂

    2024年02月16日
    浏览(37)
  • 算法 数据结构 递归插入排序 java插入排序 递归求解插入排序算法 如何用递归写插入排序 插入排序动图 插入排序优化 数据结构(十)

    1. 插入排序(insertion-sort):                                           是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入     算法稳定性:                  

    2024年02月09日
    浏览(43)
  • 【数据结构与算法】如何对快速排序进行细节优化以及实现非递归版本的快速排序?

    君兮_的个人主页 即使走的再远,也勿忘启程时的初心 C/C++ 游戏开发 Hello,米娜桑们,这里是君兮_,国庆长假结束了,无论是工作还是学习都该回到正轨上来了,从今天开始恢复正常的更新频率,今天为大家带来的内容是快速排序的两大优化和非递归实现 好了废话不多说,开

    2024年02月08日
    浏览(37)
  • 【数据结构与算法篇】手撕八大排序算法之快排的非递归实现及递归版本优化(三路划分)

    ​👻内容专栏: 《数据结构与算法篇》 🐨本文概括: 利用数据结构栈(Stack)来模拟递归,实现快排的非递归版本;递归版本测试OJ题时,有大量重复元素样例不能通过,导致性能下降,优化快速排序通过将数组划分为三个区域,可以更有效地处理重复元素。 🐼本文作者:

    2024年02月11日
    浏览(49)
  • 数据结构(C语言实现)——常见排序算法的基本思想及实现(快速排序的三种方法和优化及非递归实现快速排序)

    生活中几乎处处都会用到排序,比如:网购时的店铺顺序,学生成绩的排名等,今天我们就来学习数据结构中常见的几种排序算法。 排序 :所谓排序,就是使一串记录,按照其中的某个或某些的大小,递增或递减的排列起来的操作。 稳定性 :假定在待排序的记录序列

    2023年04月24日
    浏览(40)
  • 【数据结构】快速排序,归并排序

    1.hoare版本 根据动图的演示,整理的思路如下, 1.定义left,right,key。key默认是左边第一个元素,像两个指针,左边找比key大的,右边找比k小的,找到的话,交换二者,往返这个过程,当left与right相遇时,交换key和此时相遇的值. 单趟下来,6出现在正确的位置。 1.为什么大循环

    2024年01月20日
    浏览(30)
  • 数据结构——排序算法——归并排序

    在第二个列表向第一个列表逐个插入的过程中,由于第二个列表已经有序,所以后续插入的元素一定不会在前面插入的元素之前。在逐个插入的过程中,每次插入时,只需要从上次插入的位置开始,继续向后寻找插入位置即可。这样一来,我们最多只需要将两个有序数组遍历

    2024年02月09日
    浏览(27)
  • 【数据结构】排序(3)—堆排序&归并排序

                                      目录      一. 堆排序       基本思想         代码实现    向上调整算法    向下调整算法       时间和空间复杂度       稳定性     二. 归并排序       基本思想       代码实现       时间和空间复杂度       稳定性      

    2024年02月08日
    浏览(30)
  • 【数据结构】——归并排序和计数排序

    🌇个人主页:_麦麦_ 📚今日名言:繁华落尽,我心中仍有花落的声音。一朵,一朵,在无人的山间轻轻飘落。——席慕蓉《桐花》 目录 一、前言 二、正文 1.归并排序 1.1 基本思想 1.2【递归版】具体实现  1.3【递归版】代码部分  1.4【非递归版】具体实现  1.5【非递归版】

    2023年04月15日
    浏览(29)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包