排序算法之——归并排序

本文详细介绍了归并排序的递归和非递归实现方法,通过OJ题回顾合并操作,强调代码中注意事项,包括拷贝数组、动态开辟空间和处理边界条件,以及隐藏的bug及其修复过程。
摘要由CSDN通过智能技术生成

归并排序

  • OJ题引入
  • 排序思想
  • 代码注意事项
  • 递归方法——归并排序的实现
  • 非递归方法——归并排序的实现
    • 引入
    • 局部的分割与一次排序
    • 多次排序
    • 隐藏bug与代码完善

OJ题引入

在学习归并排序之前,我们先来回忆一下我们在顺序表阶段写过的一个OJ题——两个有序数列的合并问题,题目如下:在这里插入图片描述
这道题我们使用双指针一个新数组即可快速得到正确的答案,代码如下:

void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
    int a[m+n];
    int cur1=0;
    int cur2=0;
    int put=0;
    while(cur1<m&&cur2<n)
    {
        if(nums1[cur1]<=nums2[cur2])
        {
            a[put++]=nums1[cur1++];
        }
        else if(nums1[cur1]>nums2[cur2])
        {
            a[put++]=nums2[cur2++];
        }
    }
    while(cur1<m)
    {
        a[put++]=nums1[cur1++];
    }
    while(cur2<n)
    {
        a[put++]=nums2[cur2++];
    }
    for(int i=0;i<nums1Size;i++)
    {
        nums1[i]=a[i];
    }
}

由两个有序数组合并成为一个有序数组就是本节标题归并中的“”一字,其实会解本道题,归并排序的算法我们已经读懂了一半,下面我们具体来讲解归并排序的相关思想。

排序思想

所谓归并排序,顾名思义就是“先归后并”,归其实就是我们熟悉的递归,并也就是合并的意思先递归再合并。那我们通过引入已经知道合并操作是让书局有序的操作,那递归的作用是什么?如何递归?就是接下来我们需要讨论的重点。
如下图所示,包含了归并排序的重要步骤。在这里插入图片描述
我们将待排数据从中间分割成较小的数据,经过数次分割,数据组被我们分割成单个数据(该部操作本质上就是递归的大问题化小问题思想),可采用递归实现。当分割到一个元素时,其实一个元素就构成一个有序的数据组,此刻开始有序数组的合并操作(与上述OJ题思想一致)。
动态图片演示如下:
请添加图片描述

代码注意事项

1.进行合并时别忘记拷贝这一操作,同时由于有拷贝这一操作,我们写代码时,最好将归并操作重新用一个函数进行分装
2.拷贝时,我们要边合并边拷贝,不能所有合并结束之后才开始拷贝,这是因为没一次合并我们都会改变数组的顺序,改变之后的数组是下一次合并的数组基础。
3.合并操作时,各数组的下标要和递归所传值保持一致。

递归方法——归并排序的实现

合并操作我们需要一个新的数组,在VS环境下,我们最好采用动态开辟建立该新数组。(以升序为例实现)

void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int begin = 0;
	int end = n - 1;
	if (tmp == NULL)
	{
		perror("malloc error\n");
	}
	_MergeSort(a, 0, n-1, tmp);
	//注意动态开辟时要对空间进行释放。并对指针置空,避免野指针。
	free(tmp);
	tmp=NULL;
	
}

归并操作在我们的分装函数 _MergeSort 中实现。

void _MergeSort(int* a, int begin, int end,int* tmp)
{
	//当分割时只有一个元素或没有任何元素时,返回。
	if (begin >= end)
	{
		return;
	}
	
	int key= (begin + end) / 2;//从中间分割数组
	//[begin,key]&&[kye+1,end]为分割区间划分组
	_MergeSort(a, begin, key,tmp);
	_MergeSort(a, key + 1, end,tmp);
	//合并时,由于合并区间的可变性,注意合并区间的范围
	int begin1 = begin; int end1 = key;
	int begin2 = key + 1; int end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else {
			tmp[i++] = a[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}
	//拷贝操作一定要一次归并操作结束后立刻进行。
	memcpy(a+begin, tmp+begin, sizeof(int) * (end-begin+1));
}

非递归方法——归并排序的实现

引入

我们之前实现斐波那切数列的的非递归算法时采用的是循环法,先求和再赋值。那我们实现归并排序的算法时我们能否借助同样的思路?递归思路是由整体拆分到局部合并,而循环思路就是由局部到整体直接合并
如下图所示,循环解法就是反其道而行之,现合并红色椭圆组别,再排序蓝色椭圆组别。
在这里插入图片描述

局部的分割与一次排序

我们要进行局部的合并,首先就要考虑如何进行局部分割,考虑到合并时我们需要有序数组进行合并。所以分割时,我们考虑第一次循环一个数进行分割,建立一个gap变量进行分组。

int gap=1;
for(int i=0;i<n;i+=gap*2)
{
	......//组内进行合并排序
}

由gap建立完组别并控制了排序内部结束条件(i<n),接下来我们应该考虑每组中两个待排数组的区域划分:我们以合并排序的第一次循环一个数为一组为例:
gap=1
在这里插入图片描述
gap=2
在这里插入图片描述
由以上两组可轻易发现数组的分区规律为
第 一 个 数 组 : [ b e i g i n 1 , e n d 1 ] = [ i , i + g a p − 1 ] 第 二 个 数 组 : [ b e i g i n 2 , e n d 2 ] = [ i + g a p , i + 2 ∗ g a p − 1 ] \begin{matrix} 第一个数组:[beigin1,end1]=[i,i+gap-1]\\ 第二个数组:[beigin2,end2]=[i+gap,i+2*gap-1] \end{matrix} [beigin1,end1]=[i,i+gap1][beigin2,end2]=[i+gap,i+2gap1]
通过以上排序数组分组分析,我们能结合合并有序数组写出一次排序循环(别忘了开辟空间):
拷贝数据时,每次要拷贝 [i,end2] 个数据。

void MergeSortNouR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}
	int gap = 1;
	for (int i = 0; i < n; i += gap * 2)
	{
		int begin1 = i; int end1 = i + gap - 1;
		int begin2 = i + gap; int end2 = i + 2 * gap - 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) * (end2 - i + 1));

	}
}

多次排序

当然这仅仅是一次排序,我们要进行多次排序,那总共要进行多少次排序呢?当数组元素(gap)大于总元素个数(n)时就排序完成。代码如下:

void MergeSortNouR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			int begin1 = i; int end1 = i + gap - 1;
			int begin2 = i + gap; int end2 = i + 2 * gap - 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) * (end2 - i + 1));

		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}

隐藏bug与代码完善

那代码写到这就算结束了吗?
当我们运行这个代码时,编译器竟然报错了!!!在这里插入图片描述
这是由于,代码还有一处隐藏bug。数组的区域划分并不一定和上述一致(不一定为偶数),有可能少几个值,这时就要对区间进行限制。那考虑需要限制哪几个值呢?
begin1:由于begin1=i,已经在for循环中进行了限制无需限制;
end1:有可能大于n,大于n时代表数据已经排好;
begin2:和end1类似;
end2:end2大于n时代表数组不为偶数,按上诉分割方式并不能完刚好分割,造成越界访问,end2最大能取n-1
因此,添加限定之后的代码如下:

void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc fail\n");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += gap * 2)
		{
			int begin1 = i; int end1 = i + gap - 1;
			int begin2 = i + gap; int end2 = i + 2 * gap - 1;
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 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) * (end2 - i + 1));

		}
		gap *= 2;
	}
	free(tmp);
	tmp = NULL;
}
  • 24
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值