Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >深入理解数据结构第六弹——排序(3)——归并排序

深入理解数据结构第六弹——排序(3)——归并排序

作者头像
GG Bond1
发布于 2024-06-14 13:13:27
发布于 2024-06-14 13:13:27
9600
代码可运行
举报
文章被收录于专栏:C/C++葵花宝典C/C++葵花宝典
运行总次数:0
代码可运行

前言:

在前面,我们已经学习了插入排序、堆排序、快速排序等一系列排序,今天我们来讲解一下另一个很高效的排序方法——归并排序


一、归并排序的思想

归并排序是一种经典的排序算法,它采用了分治法的思想。分治法的核心是“分而治之”,即将一个复杂的问题分解成两个或多个相同或相似的子问题,将这些子问题逐个解决,最后将子问题的解合并以解决原问题。

归并排序的基本思想如下:

  1. 分解(Divide)
    • 将待排序的数组从中间分成两半,递归地对这两半分别进行归并排序。
    • 一直分解,直到每个子数组只包含一个元素,因为一个元素的数组自然是有序的。
  2. 解决(Conquer)
    • 当分解到最小子问题时,即每个子数组只有一个元素时,开始解决这些小问题。
    • 解决的方式是合并(Merge)两个有序的子数组,从而得到一个更大的有序数组。
  3. 合并(Merge)
    • 合并过程是归并排序的关键步骤。它将两个有序的子数组合并成一个有序的数组。
    • 通常使用两个指针分别指向两个子数组的起始位置,然后比较两个指针所指向的元素,将较小的元素放入结果数组中,并移动该指针。
    • 重复这个过程,直到一个子数组被完全合并到结果数组中,然后将另一个子数组的剩余元素直接复制到结果数组中。

归并排序的操作如下:

归并操作其实就是将一组数据通过递归等不断划分成两个部分,直到划分到一个元素之后,再对这两部分排序排进一个数组内,相当于把划分的过程再反过来走了一遍,只是走回去的过程中会把数组一步一步的有序化

二、归并排序的递归实现

递归的实现其实是很有意思的,在上面我们已经讲了递归的思想,其实就是不断的重复划分然后排序的过程,所以我们就可以设计一个递归来实现这种,同时,由于每一步都要进行分区划分,所以我们可以封装一个划分函数(_MergeSort函数)在前,重复这个过程

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);

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

	free(tmp);
}

1、因为我们在划分结束后,需要将各个小的部分再排序成一个有序的大部分,所以我们创建一个tmp的指针指向一个与原数组一样大小的空间,然后每一次排序放进这个空间,最后再把这个空间中的数据复制回原数组 2、其中_MergeSort函数内参数分别为原数组指针,首元素位置,尾元素位置,tmp指针

然后我们就来实现这个分步函数,这个函数的功能就是实现将一个数组不断分为两个部分,当划分成最小单元时,两个两个比较大小,并且放入tmp中,再复制进原数组中,我们先拿数组 { 8 ,7,6,5,4,3,2,1 } 举个例子

实现上述过程的代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//归并排序
void _MergeSort(int* a, int begin,int end,int* tmp)
{
	if (begin == end)
		return;

	//小区间优化
	if (end - begin + 1 < 10)
	{
		InsertSort(a + begin, begin - end + 1);
	}

	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1];
			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));
}

在这段代码有些部分我们在下面单独讲解一下:

小区间优化是什么及其作用

三、归并排序的非递归实现

学习完归并排序的递归实现后,我们来看一下归并排序的非递归实现

归并排序由于需要不断划分,可想而知其非递归实现是一定需要用到循环的,但是它其实还是有几个很大的坑等着我们的,归并排序的非递归实现要比其递归实现复杂的多

归并排序非递归实现需要注意的点:

1、在上面举的例子中我们都是举的2的n次方的例子,所以能恰好完全归并,但是我们实际排序时也可能遇到排11个数等,这里就比较麻烦了,所以我们需要分情况处理 2、由于上面在循环归并时次数的不确定性,所以我们每一次循环排序结束都要拷贝回原数组

如图,我们在对两个数组进行归并时是要定义两个数组的起始位置的,但是我们可能会遇到下面三种情况:

1、end1>n,即从end1开始就超出数组长度 2、end1<n,begin2>n,即从begin2开始超出数组长度 3、end2>n,即从end2开始才超出数组长度 不管这上面哪一种,都会导致我们之后的归并排序中会有数组落单,所以我们就需要针对这中情况进行处理

针对这种情况我们有两种解决方法:

1、跳出:就是当end1或者begin2大于n时跳出不进行处理 2、优化:就是将end1、begin2、end2进行优化处理,让它能够进行操作

具体操作如下:

1、跳出的思想

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//非递归的归并排序(跳出的思想)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		gap *= 2;
		int j = 0;
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			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));
		}
	}
	free(tmp);
}

2、优化的思想

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//非递归的归并排序(修正的思路)
void MergeSortNonR2(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		gap *= 2;
		int j = 0;
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;

			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[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));
		}
	}
	free(tmp);
}

四、完整的代码实例

下面我们通过排序数组{ 4,7,1,9,3,6,5,6,8,3,2,0,6 }来检验这三种方法是否成功(递归一种,非递归i两种)

SeqList.h

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include<stdio.h>
//归并排序
void MergeSort(int* a, int n);

test.c

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//测试归并排序
void TestMergeSort()
{
	int a[] = { 4,7,1,9,3,6,5,6,8,3,2,0,6 };
	int b[]= { 4,7,1,9,3,6,5,6,8,3,2,0,6 };
	int c[]= { 4,7,1,9,3,6,5,6,8,3,2,0,6 };
	printf("a数组排序前:");
	PrintArray(a, sizeof(a) / sizeof(a[0]));
	MergeSort(a, sizeof(a) / sizeof(a[0]));   //递归法
	printf("a数组排序后(递归法):");
	PrintArray(a, sizeof(a) / sizeof(a[0]));
	printf("\n");

	printf("b数组排序前:");
	PrintArray(b, sizeof(b) / sizeof(b[0]));
	MergeSortNonR(b, sizeof(b) / sizeof(b[0]));   //非递归法(跳出法)
	printf("b数组排序后(跳过法):");
	PrintArray(b, sizeof(b) / sizeof(b[0]));
	printf("\n");

	printf("c数组排序前:");
	PrintArray(c, sizeof(c) / sizeof(c[0]));
	MergeSortNonR2(c, sizeof(c) / sizeof(c[0]));   //非递归法(修正法)
	printf("c数组排序后(修正法):");
	PrintArray(c, sizeof(c) / sizeof(c[0]));
	printf("\n");
}
int main()
{
	
	TestMergeSort();
	
	return 0;
}

SeqList.c

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//归并排序
void _MergeSort(int* a, int begin,int end,int* tmp)
{
	if (begin == end)
		return;

	//小区间优化
	if (end - begin + 1 < 10)
	{
		InsertSort(a + begin, begin - end + 1);
	}

	int mid = (begin + end) / 2;
	_MergeSort(a, begin, mid, tmp);
	_MergeSort(a, mid + 1, end, tmp);
	
	int begin1 = begin, end1 = mid;
	int begin2 = mid + 1, end2 = end;
	int i = begin;
	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] < a[begin2])
		{
			tmp[i++] = a[begin1];
			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));
}
void MergeSort(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);

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

	free(tmp);
}
//非递归的归并排序(跳出的思想)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			
			if (end1 >= n || begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}

			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);
}
//非递归的归并排序(修正的思路)
void MergeSortNonR2(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	int gap = 1;
	while (gap < n)
	{
		int j = 0;
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;

			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[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);
}
//打印数组
void PrintArray(int* a, int n)
{
	for (int i = 0; i < n; i++)
	{
		printf("%d ", a[i]);
	}
	printf("\n");
}

程序运行结果:

五、总结

综合以上,我们其实就可以清楚的认识到归并排序的有趣及其思想,这篇文章并没有将归并排序的时间复杂度和适用场景等问题,我打算在后边写一个总结的文章,将这几种排序放在一起比较,给出他们时间复杂度的快慢和适用场景的不同,敬请期待吧!!!

谢谢各位大佬观看,创作不易,还请各位大佬点赞支持!!!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-04-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
数据结构从入门到精通——归并排序
归并排序是一种分治策略的排序算法。它将一个序列分为两个等长(几乎等长)的子序列,分别对子序列进行排序,然后将排序结果合并起来,得到完全有序的序列。这个过程递归进行,直到整个序列有序。归并排序的时间复杂度为O(nlogn),空间复杂度为O(n)。
鲜于言悠
2024/03/28
5170
数据结构从入门到精通——归并排序
【数据结构】排序之归并排序与计数排序
在前面的文章中介绍了 插入排序和交换排序,今天来分享的是归并排序和计数排序。 话不多说,正文开始。
zxctscl
2024/01/23
1530
【数据结构】排序之归并排序与计数排序
【数据结构】八大排序之归并排序算法
表示不小于x的最小整数)个长度为2或1的有序子序列;再两两归并,......,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序.
修修修也
2024/04/01
1590
【数据结构】八大排序之归并排序算法
【初阶数据结构】归并排序 - 分而治之的排序魔法
本文讲解的排序算法是归并排序,作为归并算法,其有着快速排序算法没有的特性,也是面试比较常考的算法之一。本文会重点讲解思路以及代码的实现。
埋头编程
2024/10/18
1000
【初阶数据结构】归并排序 - 分而治之的排序魔法
【数据结构实战】一起探索快速排序和归并排序的奥秘
上一篇我们讲了各大排序之间的差距,以及他们实现的思路和代码,本期我们将详细的研究一下快速排序和归并排序的奥秘
f狐o狸x
2024/12/24
780
【数据结构实战】一起探索快速排序和归并排序的奥秘
归并排序详解
可以看到这种结构很像一棵完全二叉树,分阶段可以理解为就是递归拆分子序列的过程,递归深度为log2n。
P_M_P
2024/01/20
1170
归并排序详解
数据结构——lesson12排序之归并排序
前面我们学习过六种排序——直接插入排序、希尔排序、直接选择排序、堆排序、冒泡排序和快速排序,今天我们就来学习归并排序🥳🎉🎉🎉
大耳朵土土垚
2024/04/04
1350
数据结构——lesson12排序之归并排序
【初阶数据结构】星河中的光影 “排” 象:排序(下)
接上一篇,解决掉剩余的排序方法,本篇有些许难度,建议巩固好上一篇再来进行本篇的学习
DARLING Zero two
2025/02/26
640
【初阶数据结构】星河中的光影 “排” 象:排序(下)
【C语言/数据结构】排序(归并排序|计数排序|排序算法复杂度)
计数排序需要我们新创建一个统计数组,按理来说,数组下标就可以用来当作统计的数,该位置就来存放该数出现的次数。但是,如果要排序的数是从一千多开始的,这样前面的空间就全部浪费了。所以我们采用相对映射的方法。即用待排序的数中,最大的数-最小的数+1就可以得到范围,从而减少空间浪费。接着用原数组的数减去最小值,将该值作为count数组的下标,即相对映射。最后进行排序,记得加回最小值min,这样数据才不会被改变。
秦jh
2024/01/31
1670
【C语言/数据结构】排序(归并排序|计数排序|排序算法复杂度)
【数据结构与算法】归并排序:从理论到实践的深度剖析
归并排序的非递归实现(也称为迭代实现)主要依赖于循环而非递归调用来分解和合并数组。与递归实现相比,非递归实现避免了递归调用栈的开销,但需要更复杂的索引和边界处理。
倔强的石头_
2024/12/06
2600
【数据结构与算法】归并排序:从理论到实践的深度剖析
排序7:归并排序
非递归做的无非就是模拟递归版本,我们用一个gap来控制下标的间隔,第一次让gap = 1。分到最细的时候每次排序是两个数字排序或者是一个数字原地不动,那么我们可以设置一个for循环,每次 i 加上两个gap的值,就做到了跳到下一个需要的排序的区间。然后每次gap的值×2,就解决了两个区间合并的问题。
青衫哥
2023/03/31
3440
排序7:归并排序
【排序算法】 归并排序详解!深入理解!思想+源码实现!
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide andConquer)的一个非常典型的应用。 将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
屿小夏
2024/01/22
7270
【排序算法】 归并排序详解!深入理解!思想+源码实现!
【数据结构】排序算法——Lesson2
本文将继续介绍两种高效的排序算法——归并排序、计算排序。 归并排序在一些场合下(如外部排序)非常有效,当数据量非常大且无法全部加载到内存时,可以将其分块处理。 而计数排序是一种非比较排序算法,适用于特定范围内的整数排序,在许多数情况下计算排序可以秒杀我们介绍过的所有排序。
_小羊_
2024/10/16
1240
【数据结构】排序算法——Lesson2
归并排序深度剖析
归并排序是建立在归并操作上的一种有效,稳定 的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 ————百度百科
用户11029129
2024/06/04
1400
归并排序深度剖析
数据结构(C语言)之对归并排序的介绍与理解
首先,归并排序可以理解为用分治策略的一种排序算法,这里可以用递归的思想去理解,对一个数组进行不断分割,每次分为两个子数组,直到最后剩下的是一个数据也就是不可再分割,那么就开始对末两个子数组进行归并,然后归回去,在原数组得到有序的数组。(也就是说再每次归并的两个数组一定要是有序的)。
羑悻的小杀马特.
2025/01/23
740
数据结构(C语言)之对归并排序的介绍与理解
DS:八大排序之归并排序、计数排序
要注意:递归的返回条件是begin==end!!因此归并排序每次拆分都是从中间拆分的,所以不可能会出现区间不存在的情况!! 只有可能是区间只有一个元素的情况
小陈在拼命
2024/02/20
1560
DS:八大排序之归并排序、计数排序
归并排序(递归+非递归)
开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情
lovevivi
2022/12/09
5330
归并排序(递归+非递归)
【初探数据结构】归并排序与计数排序的序曲
本文主要内容是归并的递归和非递归以及计数排序的实现方法。文章会提及很多容易忽视的易错点(大多是我自己踩过的坑😂),这是我在学习这块内容时获取的教训和宝贵经验。
我想吃余
2025/03/31
1000
【初探数据结构】归并排序与计数排序的序曲
排序(3)
用户11039545
2025/01/14
500
归并排序-MergeSort (C语言详解)
好久不见, 前面我们了解到了快速排序, 那么本篇旨在介绍另外一种排序, 它和快速排序的思想雷同, 但又有区别, 这就是归并排序, 如下图, 我们对比快速排序与归并排序.
用户11317877
2024/10/16
6000
归并排序-MergeSort (C语言详解)
推荐阅读
相关推荐
数据结构从入门到精通——归并排序
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验