6.4排序——归并排序

本篇博客梳理归并排序算法

一、归并排序递归版本

1.算法思想

使每个子序列有序,再使子序列之间有序,是分治法的典型应用,归并的过程发生在递归往回退的过程当中,可以类比二叉树的后序遍历
递归最小子问题:区间只有1个值或者不存在,就认为是有序了

2.具体操作

(1)归并操作

归并排序
归并操作图解如下
归并操作

(2)使左右子序列有序

使用递归即可解决,递归至最小子序列之后回归的过程会进行归并操作,递归结束之后整个序列自然也就有序了
程序架构如下


void _MergeSort(int* a,int* tmp,int begin,int end)
{
	if(begin>=end)//此时达到了递归的最小子问题:也就是区间只有1个值或者不存在
		return;
	//...递归左右子序列,递归完之后进行归并操作
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if(tmp==NULL)
	{
		perror("malloc failed!");
		return;
	}
	_MergeSort(a, tmp, 0, n - 1);
}

3.动图展示

归并排序动图
数据被拉到下面进行的操作就是归并操作,可以想象底下有一个tmp数组,begin1和begin2在上面选出更小的那个数放到tmp数组里面,这个子序列归并完成之后再拷贝回原数组

4.具体代码实现

void _MergeSort(int* a, int* tmp, int begin,int end)
{
	if (begin >= end)
		return;

	//递归到左右子序列
	_MergeSort(a, tmp, begin, (begin + end) / 2);
	_MergeSort(a, tmp, (begin + end) / 2 + 1, end);

	//归并操作
	int mid = (begin + end) / 2;
	int begin1 = begin,end1 = mid;//左子区间[begin1,end1]
	int begin2 = mid + 1,end2 = end;//右子区间[begin2,end2]

	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i] = a[begin1];
			i++;
			begin1++;
		}
		else
		{
			tmp[i] = a[begin2];
			i++;
			begin2++;
		}
		//此时可能会剩下一组数据没有完全拷贝到tmp数组当中
	}
	//两个循环只会进一个
	while (begin1 <= end1)
	{
		tmp[i] = a[begin1];
		begin1++;
		i++;
	}
	while (begin2 <= end2)
	{
		tmp[i] = a[begin2];
		begin2++;
		i++;
	}
	
	//把tmp数组拷回原数组
	//注意:dest是a + begin处位置,不然会导致前面已经拷贝好的数据被覆盖掉
	memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc failed!");
		return;
	}

	_MergeSort(a, tmp, 0, n - 1);
}

有一个细节要注意:左右子区间的划分
如果是[偶数,偶数+1]区间的mid,mid=(begin+end)/2。若分割成[begin,mid-1]和[mid,end],
会导致[mid,end]跟原区间一模一样,进而导致死循环(偶数代入2即可验证)
正确的划分方法:[begin,mid]和[mid+1,end]

二、归并排序非递归版本

1.算法思想

先从最小的子序列开始归并,然后归并的序列不断扩大
(1)单趟:走归并的逻辑,用for循环控制
(2)整体:用gap迭代控制归并的子序列规模,gap就是每组归并的数据个数。外面套一层while循环即可
具体就是:一 一归并,两两归并,四四归并…以此类推
归并排序非递归版本
随着gap的增大,左右子区间的边界就需要考虑是否越界的问题
左子区间:[begin1,end1]
右子区间:[begin2,end2]

分为以下三种情况(begin1已经受循环条件限制,不可能越界)

  • [begin1,end1],[begin2,end2]中只有end2越界
  • [begin1,end1],[begin2,end2]中,begin2,end2越界
  • [begin1,end1],[begin2,end2]中,end1,begin2,end2越界

针对后两种情况,总结起来就是右子区间不存在了,那么也就不需要进行归并了,直接break结束循环即可
对于第一种情况,右子区间还有一些数需要进行归并操作,那么只需要修正以下end2即可

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

2.具体代码实现

// 归并排序非递归实现
void MergeSortNonR(int* a, 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 += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;//左子区间[begin1,end1]
			int begin2 = i + gap, end2 = i + 2 * gap - 1;//右子区间[begin2,end2]
			if (begin2 >= n)
				break;
			//到此至少begin2没有越界
			if (end2 >= n)
				end2 = n - 1;//把end2修正一下,然后继续归并
			//归并操作
			int j = i;
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] < a[begin2])
				{
					tmp[j] = a[begin1];
					j++;
					begin1++;
				}
				else
				{
					tmp[j] = a[begin2];
					j++;
					begin2++;
				}
				//此时可能会剩下一组数据没有完全拷贝到tmp数组当中
			}
			//两个循环只会进一个
			while (begin1 <= end1)
			{
				tmp[j] = a[begin1];
				begin1++;
				j++;
			}
			while (begin2 <= end2)
			{
				tmp[j] = a[begin2];
				begin2++;
				j++;
			}
			//把tmp数组拷回原数组
			//i就相当于begin
			memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
		}
		gap = gap * 2;
	}
}

三、归并排序特性总结

1.时间复杂度:O(N·logN)
2.空间复杂度:O(N)
3.稳定性:稳定
4.缺点在于需要O(N)的空间复杂度,归并排序更多是解决在磁盘中的外排序问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值