归并排序解法一

228 阅读5分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

排序

常见的排序算法

image-20211119082822804

常见排序算法的实现

归并排序

基本思想

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

实际上归并我们不是第一次接触,之前我们也是接触过的,比如合并两个有序数组这个就是归并思想


但是我们上面的题目是左区间有序,右区间也有序。我们正常题目肯定不会直接给你有序。这时候再深一点,你不是没有序吗,那我们再分,分到你无法再分,也就是只有一个了,你能说一个没有序吗,肯定不行,所以我们继续分治。


递归写法

看上面的GIF也知道第一反应是递归

通过调试看一下现象
image-20211130224907672

归并顺序
image-20211130230648998

归并2

image-20211201014452321

归并排序递归子函数

// 归并排序递归子函数
void _MergeSort(int* a, int left, int right, int* tmp){
	//左大于右说明是空数组,空数组就跳
	//左等于右就是我们要的单体有序
	if (left >= right)
		return;
	//防溢出写法
	int mid = left + (right - left) / 2;
	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid+1,right, tmp);
	//
	int begin1 = left;
	int end1 = mid;
	int begin2 = mid + 1;
	int end2 = right;
	int i = left;
	//跑空一组就直接跳
	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++];
	}
	//把tmp数组拷贝回到原来的数组中
	i = left;
	while (i<=right)
	{
		a[i] = tmp[i];
		i++;
	}
}

归并排序递归实现

// 归并排序递归实现
void MergeSort(int* a, int n) {
	assert(a);
	//首先创建一个临时数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	//空就直接错
	assert(tmp);
	//子函数
	_MergeSort(a, 0, n - 1, tmp);
	//不用了就free掉
	free(tmp);
	//然后置空
	tmp = NULL;
}

非递归写法

2^n^个元素的数组
image-20211201012824875

image-20211201014637727

我们看到上面好像没啥问题,那是用为数组元素个数真的太有好了,一直没有落单的元素,好的不真实

image-20211201015555785

image-20211201020545678

随便几个元素的数组
修正下标

越界情况讨论

image-20211201092011579

但是出现另一种恶心情况 重复拷贝

image-20211201094210650

所以接下来我们需要解决index问题

image-20211201095510524

我们修正到n-1,同样也可以把数组修不存在,让他不进下面的循环也就可以不会进行归并

image-20211201100753211

image-20211201101244258

归并排序非递归实现        修正下标

// 归并排序非递归实现
void MergeSortNonR(int* a, int n) {
	assert(a);
	//首先创建一个临时数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	//空就直接错
	assert(tmp);
	int gap = 1;
	int i = 0;
	while (gap<n){
		for (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,end2]不存在
			if (end1 >= n) {
				end1 = n - 1;
			}
			//[begin2,end2]不存在
			if (begin2 >= n) {
				begin2 = n ;
				end2 = n - 1;
			}
			//end2出界
			if (end2 >= n) {
				end2 = n - 1;
			}
			//printf("[%d,%d],[%d,%d]",begin1,end1,begin2,end2);
			////重复拷贝基本是我们修正到同一个位置的原因
			////我们条件断点一下
			//if (begin1 <mark> end1 && end1 </mark> begin2 && begin2 <mark> end2 && end2 </mark> n-1)
			//{
			//	//随便一个代码来承接断点,一句费代码
			//	int a = 0;
			//}
			
			//tmp需要一个索引
			int index = i;
			while (begin1 <= end1 && begin2 <= end2){
				if (a[begin1] > a[begin2]) {
					tmp[index++] = a[begin2++];
				}
				else{
					tmp[index++] = a[begin1++];
				}
			}			
			//肯定还有一个没跑完
			while (begin1 <= end1){
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2) {
				tmp[index++] = a[begin2++];
			}		
			//printf("       %d", index);
		}
		//printf("\n");
		
		//一行归并完了再考回去
		for (i = 0; i < n; i++) {
			a[i] = tmp[i];
		}
		gap *= 2;
	}	
	//不用了就free掉
	free(tmp);
	//然后置空
	tmp = NULL;
}
归一部分拷一部分

我们也可以像递归那样归一半分拷贝一部分,就不需要修正了,因为修正要考虑很多边界情况,有点繁琐

image-20211201104303020

归并排序非递归实现        归一部分拷一部分

// 归并排序非递归实现
void MergeSortNonR(int* a, int n) {
	assert(a);
	//首先创建一个临时数组
	int* tmp = (int*)malloc(sizeof(int) * n);
	//空就直接错
	assert(tmp);
	int gap = 1;
	int i = 0;
	while (gap<n){
		for (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,end2]不存在
			//if (end1 >= n) {
			//	end1 = n - 1;
			//}
			////[begin2,end2]不存在
			//if (begin2 >= n) {
			//	begin2 = n ;
			//	end2 = n - 1;
			//}
			////end2出界
			//if (end2 >= n) {
			//	end2 = n - 1;
			//}
			//适用任何元素个数的核心部分
			//end1出界,[begin2,end2]不存在 都不需要归并
			if (end1 >= n || begin2 >= n) {
				//直接跳,因为是在原数组操作的不需要担心最后一个没考进去
				break;
			}
			//end2出界  需要归并  就修正
			if (end2 >= n) {
				end2 = n - 1;
			}
			//printf("[%d,%d],[%d,%d]",begin1,end1,begin2,end2);
			////重复拷贝基本是我们修正到同一个位置的原因
			////我们条件断点一下
			//if (begin1 <mark> end1 && end1 </mark> begin2 && begin2 <mark> end2 && end2 </mark> n-1)
			//{
			//	//随便一个代码来承接断点,一句费代码
			//	int a = 0;
			//}
			
			//tmp需要一个索引
			int index = i;
			while (begin1 <= end1 && begin2 <= end2){
				if (a[begin1] > a[begin2]) {
					tmp[index++] = a[begin2++];
				}
				else{
					tmp[index++] = a[begin1++];
				}
			}
			
			//肯定还有一个没跑完
			while (begin1 <= end1){
				tmp[index++] = a[begin1++];
			}
			while (begin2 <= end2) {
				tmp[index++] = a[begin2++];
			}		
			//归一部分拷贝一部分
			int j = 0;
			for (j = i; j <= end2; j++) {
				a[j] = tmp[j];
			}
			//printf("       %d", index);
		}
		//printf("\n");
		
		////一行归并完了再考回去
		//for (i = 0; i < n; i++) {
		//	a[i] = tmp[i];
		//}
		gap *= 2;
	}	
	//不用了就free掉
	free(tmp);
	//然后置空
	tmp = NULL;
}