(排序8)归并排序(非递归实现),归并排序的外部排序应用(文件排序)的简单了解

归并排序递归版本

void _MergeSort(int* arr, int left , int right, int* tmp)
{
	if (left >= right)
	{
		return;
	}
	int mid = (left + right) / 2;
	_MergeSort(arr, left, mid, tmp);
	_MergeSort(arr, mid + 1, right, tmp);
	int begin1 = left;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = right;
	int k = left;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[k++] = arr[begin1++];
		}
		else
		{
			tmp[k++] = arr[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[k++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[k++] = arr[begin2++];
	}
	memcpy(arr + left, tmp + left, sizeof(int) * (right - left + 1));
}

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

归并排序的递归并不容易发生栈溢出的情况,因为你会发现他由于是平分的,所以说接近于一棵满二叉树,所以说它的深度是非常均匀,总的深度就是logN

归并排序非递归实现

  1. 归并排序的非递归用栈的话不太能够解决,不能用栈。它的话是可以直接循环改递归的,对于归并排序的非递归控制起来特别困难,尤其是在处理边界问题之上。
  2. 归并过程的生动描述
    1. tmp需要去接受两批合格的原料(两个数组区间),何谓合格?不是要求两个数组区间个数一样,而是要求这两个数组区间内必须 是各自有序的。
    2. 在tmp内根据原料合成新产品(把两个有序数组区间归并成一个区间)
    3. 将新产品交付给客户(将tmp数组的部分区间memcpy给arr)
    4. 客户不满意,要重做一个更大的,回收产品当原料。(继续向上归并,原先的归并完后的有序数组现在需要充当新一轮大归并的 两个有序数组之一)
  3. 比如说先从简单与理想化的情况算起的话,在之前形象生动的表示归并排序过程当中,在第四点就已经讲到归并排序中客户不满意要求更大的产品,原先的产品变成了制造新的更大的产品的原料,因此原料也会变得更大。现在就定义一个局部变量gap,就是用来表示每一次tmp处理归并过程的所用原料的长度(也就是两个有序区间的长度,在实际过程当中,两个有序区间的长度不一定相同,但这边先以理想情况去看待)。
  4. 在最先进行归并的过程当中,两个有序区间肯定都是一个数,因此在最开始这个gap就是1。然后在具体执行有序区间归并过程当中,由于存在两个有序区间,因此同样需要定义四个边界begin1,begin2,end1,end2,这个跟在递归过程当中是一样的。
  5. 然后这个代码当中的外层for循环中的i表示在数组arr当中两个相邻有序区间的起点位置下标,这边既然以理想化方式认为两个有序区间的长度都是gap,因此i在for循环里面都是+=gap*2。因此begin1为i,end1为i+gap-1,begin2为i+gap,end2为i+2gap-1然后在非递归与递归当中的单趟排序都是一样的,也就是三个while循环+memcpy。
  6. 整个归并的过程的话,你可以把它想象成类似于二叉树的后序遍历,就是说首先的话是需要沉到最底,也就是说先把每一个区间分割到最小,那最小就是一个数咯。然后从最底部最小开始向上归并。在归并两个有序区间的过程当中,并不要求这两个有序区间的长度必须是一模一样的,但是必须得要求归并的这两个对象必须是有序的,这是逻辑前提。
  7. 对归并排序采用非递归的方式主要是基于:我已经事先知道归并排序的递归在分割区间的过程当中最终肯定是被分成一个一个一个的,然后在一个一个一个的基础之上,在不断的向上进行归并。那我如果用非递归的话我不如直接就从1个起步这么玩起来,一个和一个去归,归成2个…
  8. 不得不再次重复与回顾一下:归并的非递归的方法的话,它的单趟排序与递归的单趟排序是一模一样的。这也就意味着必须先得创建四个变量begin12,end12来,分别维护一下两个有序区间的头和尾。然后为了用变量表示出来begin1,begin2,end1,end2,我在外面用一层for循环,这个for的i刻画的就是在数组当中两个有序子区间(肯定是连着的)的开头。在给定gap的前提之下,在整个数组当中每2gap个元素内部就会发生一次归并,然后当前所有归并执行完毕之后,再把gap=2,然后接下来去生产更大的产品。然后这个gap从1走到2,再走到4,在走到8…然后这个gap它最大的话也就只能到达大概n/2左右,不可能到达n, 一个有序区间长度就为n,那另一个归并的有序区间是谁?所以gap<n。
  9. 然后每一次两个区间归并合并完成之后,这个gap肯定要变得更大,现在的话相当于可以把它理解成客户不满意要求更大的退回来了,原先的产品又变成更大的产品的制作原料,在理想化状态之下之前进行归并过程当中的两个有序区间的长度都是gap,那么再进行下一次归并的过程当中这个原料长度gap应该*=2。
  10. 两个原料必须得合格,原料合格的标准不在于这两个原料长度一样,而在于这两个必须得各自有序,哪怕1个与1亿个,只要这一亿个数据是有序的就可以,就OK。
void MergeSortNonR(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc failed");
		return;
	}
	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			int begin1 = i;
			int end1 = i + gap - 1;
			if (end1 >= n)
			{
				break;
			}
			int begin2 = i + gap;
			if (begin2 >= n)
			{
				break;
			}
			int end2 = i + gap + gap - 1;
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			int k = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (arr[begin1] < arr[begin2])
				{
					tmp[k++] = arr[begin1++];
				}
				else
				{
					tmp[k++] = arr[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				tmp[k++] = arr[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[k++] = arr[begin2++];
			}
			memcpy(arr + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap *= 2;
	}
}
  1. 如上:end1越界了,就没必要归并了。end1没有越界,begin2越界,也没有必要归并了。end1,begin2没有越界,end2越界了。继续归并,修正end2的值为n-1。
  2. 然后在非递归的时候,由于每一个单趟排序都是与递归是一样的,也就是三个while循环。并且三个while循环结束了之后,那最后还需要拷贝一下,然后这个拷贝的字节的个数也需要去特别注意一下,别搞错了。

归并排序的外部排序应用(文件排序)

  1. 归并排序还有一个特别牛逼的地方就在于,之前我们讲的这些排序都被称为内排序,也就是说在内存当中进行排序。归并排序还可以进行外排序,也就是说还可以在外存进行排序。所谓的外存就是磁盘,归并排序还可以用作磁盘当中排序。把思想简单讲一讲吧
  2. 那有人就要问了,我吃着没事儿干干嘛要到磁盘上去排序?想象一下场景,当数据量特别特别特别特别大的时候,就内存这么小一点的地方也根本放不下。比如说我需要对500G的数据进行排序,500G的数据不可能放在内存当中,内存里面放不下500G,数据肯定是放在磁盘里面(磁盘的存储形式其实只有一种,就是以文件的形式存储)。
  3. 对磁盘里面的数据进行排序的话,其他的排序方式都并不合适,比如说堆排序,首先堆排序必须是数组,数组支持随机访问,那你文件支持随机访问吗?
  4. 再比如说快速排序,你看左边一个指针,右边一个指针,那我还是问你文件当中的数据能不能支持你像数据这样能够随机访问的?不能吧。
  5. 但归并排序OK,如果从递归的角度去走,比如说先分成250G,250G两份,比如说我现在假设这两份小文件都已经有序了,那么就各自依次从头开始比较归并不就OK了(顺便插一句,归并排序的空间复杂度是O(N)),大致就是这么一个道理,那现在再回过来是250G的两个小文件我得确保有序,那我只能继续分这么分下去,我不是要分死了…
  6. 所以不要用归并排序的递归的这么这种倒着推。而是用非递归归并排序的这么一个思路。比如说把500G的文件给他分成500份小文件,一个文件1G,那该怎么样让1G的小文件有序?那内存1G还是放得下的呀,读到内存然后一排(这时候到能用快排了,内存里面嘛)不就OK了。
  7. 纵观整个过程,其实你会发现也是蛮慢的。确实,但这是唯一的外存排序方法。当然,实际当中也有其他方式,反正大概就这样,也几乎不是很重要。

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

絕知此事要躬行

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值