Bootstrap

归并排序(非递归)——C语言实现


在这里插入图片描述


在这里插入图片描述

🚆一、递归实现归并排序的问题

  递归实现快速排序一样,递归实现归并排序一样需要在栈上建立栈帧消耗栈空间,当递归深度过深时,就会出现栈溢出的现象。为了解决这个缺陷,本文将带来非递归实现归并排序的算法。

✈️二、利用循环实现归并排序

🍟2.1 思想

  我们利用一个新的数组,新数组和原数组等长,先1个数为一组,相邻两组进行归并,并将归并完的数拷贝回原数组,这样归并完一次后数组就变成两两有序;然后2个数为一组,相邻两组进行归并,这样归并完后就四四有序了;然后4个数为一组,相邻两组进行归并,这样归并完后就八八有序了;依次类推,8个数为一组,相邻两组进行归并 ...16个数为1组,32个数为1组...

🥪2.2 图解

在这里插入图片描述

🚢三、代码实现

void MergrSortNonR(int* arr, int n)
{
	//创建临时数组
	int* tmp = (int*)malloc(sizeof(int)*n);

	int gap = 1;//代表gap个数和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;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			int index = i;    //归并时放入临时数组的位置,从两个需要归并的区间的最左边开始

			while (begin1 <= end1 && begin2 <= end2)
			{
				//谁小谁下放至临时数组
				if (arr[begin1] < arr[begin2])
				{
					tmp[index++] = arr[begin1++];
				}
				else
				{
					tmp[index++] = arr[begin2++];
				}
			}

			//循环结束把还没放下来的数放下来,两个循环只会进去一个
			while (begin1 <= end1)
			{
				tmp[index++] = arr[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = arr[begin2++];
			}
		}
		//一轮归并完再拷贝
		for (int j = 0; j <n ; j++)
		{
			arr[j] = tmp[j];
		}
		gap *= 2;
	}

	//销毁临时数组
	free(tmp);
}

代码写完了,各位觉得这个代码有问题吗?
哈哈,当然有,问题很大,这个代码只能对2^n个数进行排序,多一个都不行

🛩️四、代码剖析

  在排序的过程中会出现三种情况,分别为右区间不存在右区间存在但是算多了左区间算多了。针对这三种情况我们怎么解决呢?

🧀4.1 右区间不存在

在这里插入图片描述我们看下用上面的代码排一下这个数组:
在这里插入图片描述

崩了???

对于上面的情况,第一轮两两归并时,最后的3就没有右区间和他归并,怎么办?
答案就是:直接break掉
有的人可能会担心,直接break掉会不会导致还有数没有归并完?
答案就是:不会
因为不存在右区间的情况一定出现在本轮归并的末尾,所以不存在会漏掉其他数。 怎么处理?
在归并前加上一个判断即可

	if (end1 >= n-1)
	{
		break;
	}

🍿4.2 右区间存在但是算多了

在这里插入图片描述
  对于上面这种情况,当进行第二轮归并时,最后的两组为[3 10]和[8 ?],其实这个时候右区间只有8一个数,但是代码在计算end2的时候其实已经计算到8的后面去了,所以会出现什么情况?
在这里插入图片描述
在这里插入图片描述

对于这种情况我们需要对end2进行修正。
怎么修正?
将end2修正为最后一个数的下标n-1

	if (end2 >= n)
	{
		end2 = n - 1;
	}

🌭4.3 右区间不存在的同时左区间算多了

在这里插入图片描述
  还是这个例子,当进行第三轮归并时,[ 1 4 7 9 ]和[ 0 2 8 10 ]进行归并,[ 3 8 10]和[???]进行归并,这里不仅缺省右区间,左区间还少一个数,但是代码在计算end1时,依旧会算到8的后面,如果我们在一轮归并完后再拷贝,就会导致我们拷贝回去的代码进一个随机数。如下图所示
在这里插入图片描述
所以我们不能等到一轮归并完之后一起将数组拷贝回原数组,应该一组归并完后立刻将数据拷贝回原数组

🚀五、修改后完整代码

void MergrSortNonR(int* arr, int n)
{
	//创建临时数组
	int* tmp = (int*)malloc(sizeof(int)*n);

	int gap = 1;//代表gap个数和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;
			int end1 = i + gap - 1;
			int begin2 = i + gap;
			int end2 = i + 2 * gap - 1;
			int index = i;    //归并时放入临时数组的位置,从两个需要归并的区间的最左边开始

			//归并过程中,右区间不存在
			//这里直接break掉,因为必定已经归到最后了
			if (end1 >= n-1)
			{
				break;
			}

			//如果右区间存在,但是算多了,需要修正
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
				//谁小谁下放至临时数组
				if (arr[begin1] < arr[begin2])
				{
					tmp[index++] = arr[begin1++];
				}
				else
				{
					tmp[index++] = arr[begin2++];
				}
			}

			//循环结束把还没放下来的数放下来,两个循环只会进去一个
			while (begin1 <= end1)
			{
				tmp[index++] = arr[begin1++];
			}
			while (begin2 <= end2)
			{
				tmp[index++] = arr[begin2++];
			}

			//一次归并完后就把归并完的数据赶紧拷贝回原数组
			//不要留着全部归并完再拷贝,文章里会讲原因
			for (int j = i; j <= end2; j++)
			{
				arr[j] = tmp[j];
			}
		}
		//不要一起拷贝
		//for (int j = 0; j <n ; j++)
		//{
		//	arr[j] = tmp[j];
		//}
		gap *= 2;
	}
	

	//销毁临时数组
	free(tmp);
}

        👆回到顶部👆

在这里插入图片描述

;