【手撕归并排序】

文章目录

  • 一、归并排序是什么?
    • 二、递归实现归并排序
      • 实现代码:
    • 三、非递归实现归并排序
        • 非递归难点1:
        • 非递归难点2:
        • 非递归难点3:
      • 四、归并排序时间复杂度


一、归并排序是什么?

归并排序是将一段区间分成若干个子问题,子问题再次分成子问题,这个是分治过程;最后分成的子问题只存在一个数时,就可以开始合并,合并的过程就是比较两个子问题的过程,合并完成后将合并的新数据拷贝到原数据即可。

二、递归实现归并排序

递归实现归并排序,就是把一个大的数组分治分治,不断分治下去成一个小的数组,
最后分治成只有一个数字为止,然后每一个数字之间两两合并成2个数字,两组数组的两个数字之间再合并成4个数字,以此类推,知道合并成最后一个大的数组为止。
在这里插入图片描述
第一步:通过left和right下标找到数组中间位置的下标,以该下标为界限,划分成两组数据。

在这里插入图片描述

第二步:重复第一步的过程,但是先把左边的组彻底分完,再分右边的组,是二叉树的前序遍历的思想。

在这里插入图片描述
第三大步:不断进行分治,直到分解到还剩一个元素时停下来,判断只有一个元素,就是当left>=right时。
在这里插入图片描述
第四步:两两比较,四四比较合并

注意:每次合并完都需要把tmp的数据拷贝回原数组。

最后一步:两个子区间合并成总的区间:
注意:每次合并完都需要把tmp的数据拷贝回原数组。

实现代码:


void _MergeSort(SortDataType* a, int left, int right, SortDataType* tmp)
{
	if (left >= right)
	{
		return;
	}

	int mid = (left + right) >> 1; // 右移一位相当于/2

	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;

	int index = left; // tmp的下标,不能从0开始,因为有些归并是不会从0开始的。
	_MergeSort(a, begin1, end1, tmp);
	_MergeSort(a, begin2, end2, tmp);


	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[index++] = a[begin1++];
		}
		else
		{
			tmp[index++] = a[begin2++];
		}
	}

	//到这里不知道是谁先结束的,所以都要判断

	while (begin1 <= end1)
	{
		tmp[index++] = a[begin1++];
	}

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

	//拷贝回去

	//for (int i = left; i <= right; ++i)
	//{
	//	a[i] = tmp[i];
	//}
	// source, destination , size

	//每次归并完都拷贝一次
	memcpy(a + left, tmp + left, sizeof(SortDataType) * (right - left + 1));
}

void MergeSort(SortDataType* a, int n)
{
	SortDataType* tmp = (SortDataType*)malloc(sizeof(SortDataType) * n);

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

}

三、非递归实现归并排序

对于递归实现归并排序来说,是把大问题分成小问题,是自上往下分的。

而对于非递归来说,是从小问题开始合并成大问题,是从下往上分的。

以上面的数字为例:
大致思路如下:
在这里插入图片描述

非递归难点1:

但面临第一个问题:
如何选择从一一开始比较到两两开始比较

选择用gap
gap表示每次归并时每组的数据个数
初始时gap = 1,表示第一次是一一比较,每合并完一轮,gap*2,下一轮进行两两比较,以此类推。

非递归难点2:

不过,第二个理解的难点在于:begin1和end1,begin2和end2该如何选择的问题!

在这里插入图片描述
首先是i每次跳跃2×gap,因为一开始是一一比较,比较完一次相当于比较了两个数据,
而gap的含义就是每次合并时每组的数据个数!
那么就需要跳过2 ×gap的长度。

其次是begin1 和end1,begin1 = i 好理解;
end1 = i+gap-1是这样的:i+gap表示从begin1开始的往后的gap个数据, 由于是数据,那么-1才是下标。
而begin2 = i+gap也好理解,end1的后面一个就是begin2;
end2 = i+2*gap-1,就是从i位置开始,跳跃2×gap的数据个数到达最后一个需要比较的数据,-1就是这个最后的数据的下标。

非递归难点3:

难点3在于边界如何处理

先讲讲归并完一串数字如何拷贝回原数组:
1.一次性拷贝法,也叫梭哈拷贝法(不推荐)
2.每合并一次,就拷贝一次(推荐)

1.梭哈拷贝法:就是到合并完所有的数据之后再一次性拷贝回原数组,简单粗暴。
在这里插入图片描述

2.每合并一次就拷贝一次:在一一合并成两个有序数据之后,就拷贝会原数组。
在这里插入图片描述

这里的边界有三种情况:
第一种:end1越界了,如下情况,当合并到四四比较时,begin1刚好为末位置,那么end1开始都越界了:
在这里插入图片描述
这里的处理方法有两种,但不同的方法是根据如何将归并好的数据拷贝回原数组决定的。

如果是梭哈拷贝法,不管哪种情况,都要修正过来。

先说end1越界的情况,如果是采用梭哈拷贝法一次性拷贝会原数组,就要让end1修正到
end1 = n-1 ,让begin2和end2修正到一个不存在的区间,比如:
begin2 = n ,end2 = n-1。这样做的目的是不让begin2、end2这个区间进入循环,防止拷贝到界外的数据。
如下:
在这里插入图片描述

begin2 和end2的修正当然不唯一,只要修正到一个不存在的区间即可。

第二种:begin2越界

可能发生的begin2越界如下:

在这里插入图片描述

第二种情况处理方式与第一种相同,在梭哈拷贝法的前提下,需要修正begin2 、end2这两个数据到一个不存在的区间,防止它们被拷贝。
比如:begin2 = n,end2 = n-1。
如下:
在这里插入图片描述

第三种:end2越界

在这里插入图片描述
此时只需要把end2修正到n-1位置即可,
如下:
在这里插入图片描述

注意:begin1是不可能越界的,begin1是不可能越界的,begin1是不可能越界的,因为如果begin1越界了,那后面的end1,begin2,end2全都越界了,那还归并啥!

梭哈写法的代码如下:

void MergeSortNonR(SortDataType* a, int n)
{
	SortDataType* tmp = (SortDataType*)malloc(sizeof(SortDataType) * n);
	assert(tmp);
	int gap = 1;
	//gap 是归并过程中,每组数据的个数

	while (gap < n)
	{
		for (int i = 0; i < n; i+=2*gap)
		{
			//理解难点
			//当gap为2时,i每次都会走2步,相当于跳过一个归并组
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int index = i;

			//梭哈修正写法,但是不推荐
			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}

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

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

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			//到这里不知道是谁先结束的,所以都要判断

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[index++] = a[begin2++];
			}
		}
		//不推荐
		//法1:梭哈法:一次性整体拷贝 
		memcpy(a, tmp, sizeof(SortDataType) * n);

		gap *= 2;
	}

	free(tmp);
	tmp = NULL;	
}

二、如果是每归并一次,就拷贝一次数据回到原数组的拷贝方法的话,处理情况就不同。

在合并一次拷贝一次的情况下:

1.end1 越界了

在这里插入图片描述
因为是合并一次拷贝一次,则前面的红色的数据已经全部从tmp临时数组拷贝回到原数组了,至于3这个数据,不需要再拷贝到tmp了,让他留在原来的地方即可。
所以处理方法是直接break

2.begin2 越界了
在这里插入图片描述
与end1越界的情况相同,因为是合并一次拷贝一次,则前面的红色的数据已经全部从tmp临时数组拷贝回到原数组了,至于后面的数据,不需要再拷贝到tmp了,让他留在原来的地方即可。
所以直接break

3.end2越界

在这里插入图片描述
同样的,如果是end2越界,就需要修正end2到n-1位置,保证begin1 和begin2可比即可。
所以修正 :end2 = n-1

走一步拷贝一步的非递归写法如下:

void MergeSortNonR(SortDataType* a, int n)
{
	SortDataType* tmp = (SortDataType*)malloc(sizeof(SortDataType) * n);
	assert(tmp);
	int gap = 1;
	//gap 是归并过程中,每组数据的个数

	while (gap < n)
	{
		for (int i = 0; i < n; i+=2*gap)
		{
			//理解难点
			//当gap为2时,i每次都会走2步,相当于跳过一个归并组
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			int index = i;

			//法2:三种情况,但是前两种情况可以使用相同的方法解决
			
			//如果end1越界了,那就不归并了,
			//如果begin2越界了,那也不归并了
			if (end1 >= n || begin2 >= n)
			{
				break;
			}

			//如果end2越界了,让end2修正到n-1位置
			if (end2 >= n)
			{
				//修正
				end2 = n - 1;
			}

			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2])
				{
					tmp[index++] = a[begin1++];
				}
				else
				{
					tmp[index++] = a[begin2++];
				}
			}

			//到这里不知道是谁先结束的,所以都要判断

			while (begin1 <= end1)
			{
				tmp[index++] = a[begin1++];
			}

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

			// destination  source  size

			//推荐
			//法2:归并一点,拷贝一点,需要画图理解
			//如果是end1 或begin2大于等于n的时候越界
			//不同于梭哈一次性拷贝,梭哈拷贝需要把所有的拷贝进tmp,必须再拷回去,虽然做了无用功,但是是必须做的,这也是比较挫的地方
			//这个法2没做无用功,既然end1或者begin2越界了,那就干脆不拷贝了
			memcpy(a + i, tmp + i, sizeof(SortDataType) * (end2 - i+1));
		}
		gap *= 2;
	}

	free(tmp);
	tmp = NULL;	
}

注意两种写法中,拷贝的代码放在了while循环的不同位置!

四、归并排序时间复杂度

归并排序具有稳定性,即对于两个及以上的相同数据,归并排序前后不会改变相同数据的相对位置,这个就是稳定性。

归并排序对数据的顺序是不敏感的。

归并排序时间复杂度为O(NlogN),从一一归并开始,每次归并都需要遍历所有数据,但由于是二路归并,所以n个数据的 ”高度“是logN,即没进行一层,就需要遍历一次所有数据,所以时间复杂度就是O(NlogN).

空间复杂度:O(N),因为需要开辟一个临时数组来保存合并好的值,所以空间复杂度是O(N).

你可能感兴趣的:(数据结构和算法,算法,排序算法)