【数据结构】归并排序的递归与非递归形式

本文深入探讨了归并排序的两种实现方式:递归和非递归。归并排序是一种基于分治法的排序算法,通过递归将序列划分为有序子序列,然后合并。递归实现中,关键在于_MergeSort子函数的递归调用及区间划分。非递归实现则通过循环逐步扩大比较区间,避免了数组越界问题,分为部分拷贝和整组拷贝两种策略。两种方法都保证了时间复杂度为O(N*logN)。
摘要由CSDN通过智能技术生成

归并排序的递归:

归并排序是建立在归并操作上的一种有效的排序算法,

该算法是采用分治法(分治的思想一般通过递归去实现);

将有序的子序列(前提是有序,若无序,则要先令其有序)合并,

得到完全有序的序列;

即先使每一个子序列有序,再使子序列段之间有序。

最后将俩个有序表合并成一个有序表;

如图所示:

先让左右子树去解决同样的问题,然后得到结果之后,再整合为整颗树的结果

  • 思路:

归并函数需要用到递归,

而且还需要自定义一个归并函数的子函数 _MergeSort (),它的参数同主函数会有区别

在这个子函数中完成递归的全过程,

具体步骤如下:

先在主函数 MergeSort ()中 malloc 一个数组空间,

用以存放经过 归并 + 排序 后的有序表,

然后调用子函数 _MergeSort ()

在子函数 _MergeSort ()中,

创建一个变量 mid 用来划分左子区间与右子区间,

[ begin, mid ] [ mid+1, end ]

这里注意右子区间的 mid 要加上1,这里是一个易错点,

然后就是要通过在子函数中递归俩次子函数(递归俩次的子函数的参数不同)划分左子树与右子树,

接下来就是要进行 归并 + 排序 的操作,

其中通过之前划分到单个数据时,左右子区间(此时一个区间中有一个数据也可被称作区间),

进行比较大小的操作(以升序为例),

俩个子区间在进行比较大小时的思路:

一个子区间的 begin1 ,与另一个子区间的 begin2 先进行比较,

然后 begin1 与 begin2 分别++,用以比较接下来俩个子区间中的其它数据。

最后不要忘记将 malloc 数组空间内已经排好顺序的数据拷贝回原数组中

  • 归并排序的时间复杂度:

N * logN

  • 代码实现:
//归并排序:

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

void _MergeSort(int *a, int begin, int end,int *tmp)/*tmp是归并过程中
													要求存在的一个额外的数组,
													而 a 是存放有原数据的数组*/
{
	if (begin >= end)
		return;

	int mid = (begin + end) / 2;//将数列化分成左右俩个子区间

	// [begin, mid] [mid+1, end] 分治递归,让子区间有序
	_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 = begin1;
	
	while (begin1 <= end1 && begin2 <= end2)//这里以升序为例
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1++];//为了接近while()中的控制条件
			                       
			/*因为此时这俩组数据满足升序的情况,
			需要去找下一个数据来看是否与另一组数据满足升序的情况*/
		}
		else
		{
			tmp[i++] = a[begin2++];
		}
	}

	while (begin1 <= end1)/*下面的俩个 while 循环程序只会执行到其中一个,
						  因为在单趟比较的过程中,一定有其中一个区间(左或右)
						  被比较到最后,而此时另一个区间剩余的数则无需进行比较,
						  直接把改区间的剩余部分下放到临时的数组中即可*/
	{
		tmp[i++] = a[begin1++];
	}

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

	// 把归并数据拷贝回原数组
	memcpy(a + begin, tmp + begin, (end - begin + 1)*sizeof(int));
	/*这样的拷贝比使用 for 循环拷贝更方便,更简洁*/
}

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

	_MergeSort(a, 0, n-1, tmp);//n-1是数列区间的边界,不要误写为 n
	
	free(tmp);
}

归并排序的非递归:

首先要明确:

归并排序的非递归分为俩种:

  1. 部分拷贝
  2. 整组拷贝

这里归并排序的非递归实现是利用循环去代替递归的;

不使用数据结构中的 栈 和 队列去实现归并排序的非递归的原因:

用栈,队列去实现非递归适合前序遍历而不适合后序遍历,因为出栈后还要在归并的过程中再次使

用出栈后的子区间

非递归排序的思路:

给定一个数列对其进行排序;

先俩俩之间(俩俩相邻的数字)进行排序使其成为多个有序的数列;

然后因为此时俩俩之间有序;

所以将比较的范围扩大到俩俩相邻的有序区间(一个区间含俩个数字);

比较过后,再次扩大区间,

即此时一个所要比较的区间含四个数字

然后再次扩大区间内含有的数据个数,直至最后一个区间可以包含整个数列

注意:

每一个区间在一趟过程中只需要比较一次即可,这里的 “ 俩俩 ” 不同于常规认知的 “ 俩俩

所以,

出现部分拷贝与整组拷贝这俩种非递归代码的原因:

当左(右)区间不存在时,

因为子区间的扩张是按整数倍计算的,

而且是俩个相邻的子区间之间的

俩俩不重复比较,所以是会有数组越界的情况出现

  • 越界情况一:左子区间存在,右子区间不存在

 

该种情况的解决方法:

无需对右区间不存在的左区间进行归并,直接将左子区间放入原数组即可,(部分拷贝)

修改区间的边界(整组拷贝)

  • 越界情况二:右子区间存在,但是算多了

或者是: 

解决方法是:

缩小到实际的范围即可

 

  • 越界情况三:左子区间存在一部分,而右子区间不存在

 

解决方法:

缩小到实际的区间范围

  • 部分拷贝:

不需要修改边界,而是在每一次循环中就将不参与归并排序的子区间放入 malloc 出来的数组空间

//归并排序的非递归(部分拷贝):
void MergeSortNonR(int* a, int n)//其中 n 是待排序中数据的个数
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

	int gap = 1;//gap的值是每组中数据的个数
	
	while (gap < n)
	{
		
		for (int i = 0; i < n; i += 2 * gap)//每一次循环的区间数扩大二倍
		{
			// [i,i+gap-1][i+gap, i+2*gap-1]
			int begin1 = i, end1 = i + gap - 1;
			
			int begin2 = i + gap;			
			int end2 = i + 2 * gap - 1;//修正边界的关键

			// end1越界或者begin2越界,则可以不进行归并的过程了

			/*因为如果是整组拷贝而且不去进行边界的修正时,end1 与 begin2 的值会超出数组的边界:
			memcpy(a + i, tmp + i, sizeof(int)* n);//当里面乘以的是 n 时,
			应该是哪一部分不归并就把哪一部分拷贝下来(部分拷贝无需进行边界的修整),
			而不是整组(即包括了不应该归并的,也包括了要归并的)去进行拷贝,
			因为会把边界值与随机值的比较结果放到要进行拷贝的整个数组中,
			此时尽管不需要修正边界,但也不会出现数组越界的误差。*/
			
			if (end1 >= n || begin2 >= n)
			{
				break;/*无需进行递归,说明右子区间不存在,也无需修整边界,因为这是部分拷贝*/
			}
			else if (end2 >= n)/*如果执行到该行代码,
							   说明一定是归并过程中的右半区间存在,但是算多了的情况
							   (算多的原因是因为子区间会随着每次循环扩大含有的数据个数)*/
			{
				end2 = n - 1;
			}
			//printf("[%d,%d] [%d, %d]--", begin1, end1, begin2, end2);

			int m = end2 - begin1 + 1;//无需归并的区间
			
			int j = begin1;			
			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) * m);//注意里面乘以的是 m ,不是 n
		}   //for循环的范围内

		gap *= 2; //每一次循环的区间数扩大二倍
	}

	free(tmp);//最后不要忘记释放 malloc 出来的动态内存空间的地址
}

  • 整组拷贝:

需要修整该边界使其避免数组越界,使其多余的数组空间不会参与归并排序,从而为了后面的整组

拷贝奠定了条件

//归并排序的非递归(整组递归):
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int)*n);
	if (tmp == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}

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

			// 越界-采用修正边界的方法
			if (end1 >= n)
			{
				end1 = n - 1;
				// [begin2, end2]修正为不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				// [begin2, end2]修正为不存在区间
				begin2 = n;
				end2 = n - 1;
			}
			else if(end2 >= n)
			{
				end2 = n - 1;
			}

			//printf("[%d,%d] [%d, %d]--", begin1, end1, begin2, end2);

			int j = begin1;
			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++];
			}
		}

		printf("\n");
		memcpy(a, tmp, sizeof(int) * n);/*脱离 for 循环后一次性进行整组的拷贝,
										类比于其递归形式最后的整组拷贝*/
		/*不同于在 for 循环里面的部分拷贝,sizeof(int) 要乘以的是 n */

		gap *= 2;
	}

	free(tmp);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值