归并排序——动图+逐步讲解(C语言)

🔆欢迎来到我的【数据结构】专栏🔆

  • 👋我是Brant_zero,一名学习C/C++的在读大学生。
  • 🌏️我的博客主页​​​​​​➡➡Brant_zero的主页
  • 🙏🙏欢迎大家的关注,你们的关注是我创作的最大动力🙏🙏

🍁前言

        在学习了堆排序、希尔排序、快速排序之后,我们再来学习一个同一量级的高效排序——插入排序。

        码字不易,如果感觉写的不错的话可以点点赞、留个言;下期准备出一个八大排序的代码合集版,方便查询,感兴趣的可以关注等待一波。如果有问题和错误可以在评论区提出,我必将及时回复。

目录

一、基本思想

二、归并排序(递归)

三、归并排序(非递归)

3.1 实现

3.2 越界处理


一、基本思想

归并排序( Merge-Sort )是建立在归并操作上的一种有效的排序算法 , 该算法是采用分治法 的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有 序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
我们先来看看动态的排序过程是什么样的。

算法思路:

  1. 把数组从中间划分成两个子数组
  2. 一直递归地把子数组划分成更小的数组,直到子数组里面只有一个元素
  3. 依次按照递归的返回顺序,不断合并排好序的子数组,直到最后把整个数组顺序排好。

二、归并排序(递归)

不难发现,归并排序的递归过程十分类似于二叉树的后序遍历,先将区间分为左区间和右区间,划分到不能继续划分时,进行归并。

实习步骤:

  1. 创建一个临时数组,在临时数组中进行归并,防止归并时将原顺序打乱,整体归并完之后再将数据拷贝回原数组。
  2. 将区间划分为[begin , mid ] [mid+1,end]两段子区间,进行后序遍历递归,当划分到只有一个元素时返回。
  3. 进行归并。使用begin1和end1控制左区间,begin2和end2控制右区间,比较两个区间中的值将其插入到temp数组中,当其中一个区间归并结束时停止。
  4. 将未排完序的数组接着全放入temp数组中。
  5. 最后将temp中的值拷贝到原数组中。

void _MergeSort(int* a, int begin, int end, int* temp)
{
	if (begin >= end)
		return;
	//将区间分为左右两半
	int mid = (begin + end) / 2;
	//[begin,end]--->[begin , mid ] [mid+1,end]
	//开始递归拆开
	_MergeSort(a, begin, mid, temp);
	_MergeSort(a, mid+1, end, temp);

	//合并
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	//i要从相对位置起
	int i = begin1;
	//如果两个区间有一个结束,就停止比较归并
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			temp[i++] = a[begin1++];
		}
		else
		{
			temp[i++] = a[begin2++];
		}
	}
    //使用两个while将未归并完的数组进行追加
	while (begin1 <= end1)
	{
		temp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		temp[i++] = a[begin2++];
	}
    //拷贝回去要使用相对位置
	memcpy(a+begin, temp+begin, (end-begin+1)*sizeof(int));
}

void MergeSort(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	//使用一个子函数进行排序
	_MergeSort(a, 0, n - 1, temp);
	free(temp);
}

易错点:

  1. 因为我们进行归并都是相对位置归并,比如将区间[3,3]和[4,4]中的内容归并,那归并有序后区间是放在temp[3,4]中,所以要使用当前begin的相对位置。
  2. 这点在进行归并时要注意,即 i = begin ,表示合并到temp数组中相对的位置
  3. 同样在拷贝数据时也要注意,是从当前temp的相对位置拷贝到a数组的相对位置处即memcpy(a + begin, temp + begin, (end - begin + 1) * sizeof(int)) 而不是memcpy(a , temp, (end - begin + 1) * sizeof(int));这样将temp中的值拷贝回去就不是相对区间,还不理解的话建议画图走读代码。
  4. 最后就是memcpy的使用,第一个参数是目标地址,第二个参数是源头地址,第三个参数则是拷贝的内容大小。
  5. 注意,子函数中使用的是闭区间进行的,所以计算数组大小要多加上n(老爷爷般的唠叨)。

效果演示:

三、归并排序(非递归)

3.1 实现

 归并的非递归没有使用栈,而是使用迭代的方式,类似于求斐波那契数列,我们先实现测试版,然后再进行优化,我们开始把。

先来看看流程图:

实现思路:

  1. 模拟最后一递归中排列间距为1的子序列,例[0,0]和[1,1]……,即最后一层的归并,然后再进行间距为2的子序列,直到归并完成;
  2. 寻找规律推出左、右区间的公式:  [i , i+gap-1] [i+gap,i+2*gap-1],然后就是正常的递归合并.
  3. i 的变化是i+=2*gap,表示每次跳过两个区间,进入下一段区间.
  4. 当gap每次*=2 , 表示进入了下一层递归 , 直到gap >= n 时停止.
  5. 将临时数组拷贝回原数组.

代码如下(建议自己尝试实现):

void MergeSortNonR(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	//i 是要两组两组的跳过
	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;
			//i要从相对位置起
			int j = begin1;
			//如果两个区间有一个结束,就停止比较归并
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					temp[j++] = a[begin1++];
				}
				else
				{
					temp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				temp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				temp[j++] = a[begin2++];
			}
		}
		memcpy(a, temp, n * sizeof(int));
		gap *= 2;
	}
}

3.2 越界处理

可是非递归版本的真的这么容易实现吗,我们添加两个数并打印其边界,看看下面归并过程中有没有出现越界的情况吧。

修正边界法:

不难发现,其中越界的有end1、begin2、end2,end1是不会越界的。那我们将其进行边界的修正,这样即不会影响正常的归并,也不会将出错的情况算入其中。

处理方法:

  • 如果end1越界,则将其修改为n-1。
  • 如果begin2和end2越界,则将其修正为不存在的区间,不存在则直接跳出了归并的过程。
  • 共用三种出界情况,所以可以使用else if 判断,防止相同语句重复执行
  • 注意,最后一种end2 >= n也要使用else if  使用else会将正常的end2进行了修改。
void MergeSortNonR(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	//i 两组两组的跳过
	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 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}

			int j = begin1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					temp[j++] = a[begin1++];
				}
				else
				{
					temp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				temp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				temp[j++] = a[begin2++];
			}
		}
		memcpy(a, temp, n * sizeof(int));
		gap *= 2;
	}
}

多步拷贝法:

 多步拷贝法是在如果end1 越界或者 begin2越界,则不归并,只有end2越界时将其修正一下,所以我们要做的就是将拷贝放入循环中,每次归并都进行拷贝,这样才不会丢失数据。

void MergeSortNonR2(int* a, int n)
{
	int* temp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	//i 是要两组两组的跳过
	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;
			//end1 越界或者 begin2越界,则不归并
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
            //只有end2越界时将其修正一下
			else if (end2 >= n)
			{
				end2 = n - 1;
			}
			//记录要拷贝的个数
			int m = end2 - begin1+1;
			//i要从相对位置起
			int j = begin1;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					temp[j++] = a[begin1++];
				}
				else
				{
					temp[j++] = a[begin2++];
				}
			}
			while (begin1 <= end1)
			{
				temp[j++] = a[begin1++];
			}
			while (begin2 <= end2)
			{
				temp[j++] = a[begin2++];
			}
            //每归并一次就拷贝一次
            //使用m进行记录当前归并了多少数。
			memcpy(a+i, temp+i, m*sizeof(int));

		}
		gap *= 2;
	}
}

总结

归并排序并不难,难是是它非递归的版本。其中越界的多种情况要我们结合着调试和代码去看,我将两种修正过的代码也贴在上面了,初学强烈推荐先自己推导一遍,加深记忆,然后再看我上面的代码。其中有一些细节点我已经尽量说详细了,或者是用注释标注起来了,相对来说看懂应该不难,只是一些细节处让人十分头痛。

本篇文章到此就结束了,八大排序中难的已经都被我们解决了,剩下准备出个代码纯享的合集,方便日后查阅,如果有兴趣的可以关注一下,你们的鼓励是我创作的最大动力。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Brant_zero2022

素材免费分享不求打赏,只求关注

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

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

打赏作者

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

抵扣说明:

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

余额充值