数据结构算法—归并排序

概念

归并排序是一种基于分治思想的高效稳定排序算法,核心思路是将待排序序列递归拆分为若干子序列,直到每个子序列只有一个元素(天然有序),再依次将有序子序列两两合并,最终得到完整有序序列

  • 核心步骤:1. 分解:递归把数组从中间分成左右两部分,直至每个子数组长度为 1;
    1. 合并:从最小有序单元开始,两两比较并按顺序合并,逐层向上归并为完整有序数组

生活中的例子

  • 多人分组整理扑克牌:先把牌分给多人各自排好,再把多组有序牌依次合并成一整副有序牌
  • 多队排队合并:两队已按身高排好的队伍,由工作人员逐个对比排头,按顺序合并成一队
  • 书籍页码整理:先把厚书拆成多叠分别按页码排好,再把多叠有序页码逐叠合并成完整顺序
  • 快递分拣:先按区域分成小堆并各自排序,再把各区域有序包裹合并成整体有序的配送清单

归并排序的思路分析

第一步:拆

想象你手里有一副乱牌:10 6 7 1 3 9 4 2,想把它从小到大理整齐,归并排序就是这么干的

先把牌堆从中间劈成两半,再把每一半继续劈成更小的堆,直到每堆只剩一张牌------ 毕竟单张牌天生就是 "排好序" 的

  • 原堆:10 6 7 1 3 9 4 2
  • 第一次劈:[10 6 7 1][3 9 4 2]
  • 继续劈:[10 6] [7 1][3 9] [4 2]
  • 最后劈到最小单位:[10] [6] [7] [1] [3] [9] [4] [2]

第二步:两两合并

现在从最小的牌堆开始,两两合并成有序小堆,就像两队排好的人,挨个比排头,把小的先拉出来:

  1. 先合并单张牌:
    • 106 → 排成 6 10
    • 71 → 排成 1 7
    • 39 → 排成 3 9
    • 42 → 排成 2 4
  2. 再合并这四个小堆:
    • 6 101 7 → 挨个比排头,排成 1 6 7 10
    • 3 92 4 → 挨个比排头,排成 2 3 4 9
  3. 最后合并两个大堆:
    • 1 6 7 102 3 4 9 → 继续比排头,最终得到 1 2 3 4 6 7 9 10

一句话总结

先把乱牌拆到 "每张都孤单",再两两凑成整齐的小堆,最后把小堆一步步拼成整副整齐的牌 ------ 这就是归并排序的核心思路

错误写法

复制代码
void _MergeSort(int* arr, int* tmp, int begin, int end)
{
	if (begin == end) 
	{
		return;
	}
	int mid = (begin + end) / 2;
	//将数组区域划分为:[begin, mid - 1] [mid, end]
	_MergeSort(arr, tmp, begin, mid - 1); 
	_MergeSort(arr, tmp, mid, end);       
 
	////归并
	int begin1 = begin;
	int end1 = mid - 1;
	int begin2 = mid;
	int end2 = end;
	int i = begin; 
 
	while (begin1 <= end1 && begin2 <= end2) 
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[i++] = arr[begin1++];
		}
		else
		{
			tmp[i++] = arr[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}
	memcpy(arr + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
 
//归并排序
void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n); 
	assert(tmp != NULL);
 
	_MergeSort(arr, tmp, 0, n - 1); 
	free(tmp);
	tmp = NULL;
}

这个报错就是栈溢出,为什么会出现这样的情况呢?

我们会发现栈溢出问题的出现就是因为中间值下标mid取的有问题 ,我们知道整型的除法是会取整的,所以奇数的取中是会有问题的;而且当递归只剩下两个数的时候,下标之和是一定为奇数的,这样就会导致如图所示 mid, end 这个区间会一直不变导致无限递归的情况,而递归一次就需要在栈区开辟新的空间,无限的递归就会导致栈溢出的问题

正确写法

复制代码
void _MergeSort(int* arr, int* tmp, int begin, int end)
{
	if (begin == end) 
	{
		return;
	}
	int mid = (begin + end) / 2+1;
	//将数组区域划分为:[begin, mid - 1] [mid, end]
	_MergeSort(arr, tmp, begin, mid - 1); 
	_MergeSort(arr, tmp, mid, end);       
 
	////归并
	int begin1 = begin;
	int end1 = mid - 1;
	int begin2 = mid;
	int end2 = end;
	int i = begin; 
 
	while (begin1 <= end1 && begin2 <= end2) 
	{
		if (arr[begin1] < arr[begin2])
		{
			tmp[i++] = arr[begin1++];
		}
		else
		{
			tmp[i++] = arr[begin2++];
		}
	}
	while (begin1 <= end1)
	{
		tmp[i++] = arr[begin1++];
	}
	while (begin2 <= end2)
	{
		tmp[i++] = arr[begin2++];
	}
	memcpy(arr + begin, tmp + begin, (end - begin + 1) * sizeof(int));
}
 
//归并排序
void MergeSort(int* arr, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n); 
	assert(tmp != NULL);
 
	_MergeSort(arr, tmp, 0, n - 1); 
	free(tmp);
	tmp = NULL;
}

现在我们来测试一下

归并排序非递归版本

概念

归并排序非递归实现(自底向上归并排序),不使用递归调用与系统栈,直接从最小有序子序列开始,通过迭代逐层合并,最终得到完整有序序列

生活例子

假设你有8本散乱的书,要按书名首字母(A-Z)排序,用归并排序非递归的思路就是:

  1. 初始gap=1(单本书为一个"有序子序列"):每本书单独放,此时每一本都是"有序"的(只有1个元素,天然有序);

  2. gap=2(合并相邻2本):把第1和第2本对比排序、第3和第4本对比排序、第5和第6本对比排序、第7和第8本对比排序,得到4组"2本有序的书";

  3. gap=4(合并相邻4本):把前4本(两组2本有序的书)合并成1组4本有序的书,后4本同理,得到2组"4本有序的书";

  4. gap=8(合并相邻8本):把两组4本有序的书合并,最终得到8本按首字母排序的完整有序书本,排序完成

所以此时我们不再把一个数组看成一个整体,由于一次归并排序是需要两个有序数组,所以我们将第一排两两为一组进行归并排序,第一排也叫做11归并;当第一排完成归并排序后就如第二排所示,此时由于一个数组此时存放两个数,则相当于4个数为一组进行归并排序,所以第二排也叫做22归并;同理下面的逻辑也是如此,则我们就能不用递归来实现归并排序。

我们会发现每完成一排的归并后数组的数据量都进行了翻倍,但是我们知道归并排序是需要两个头指针同时进行比较排序,所以这两个头指针之间的距离是会发生变化的,也就是说我们需要一个变量来控制这个距离,我们就定义为 gap 。

gap指的就是当前一排中一个数组所存放的数据个数。当 gap = 1时相当于第一排,此时每两个数组为一组进行归并排序,全部完成后则gap = 2相当于第二排,此时再每两个数组为一组进行归并排序,以此类推。

2、数组中数据个数为2^n的代码实现

为什么我们先实现个数为2^n的数组排序呢?就由上面的图所示,我们能保证每两个数组都能成为一组进行归并排序,如果数组个数不是2^n,比如假设为10个数据,则到第二排的数组个数为5个,无法做到每两个数组为一组进行归并排序

归并排序非递归代码

复制代码
void MergeSortNonR(int* a, int n)
{
	int* tmp = malloc(sizeof(int) * n);
	if (tmp == NULL)
	{
		perror("malloc");
		return;
	}
	int gap = 1;//gap每组归并的个数
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			//[begin1,end1][begin2,end2]
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + 2 * gap - 1;
			if (end1 >= n)
			{
				end1 = n - 1;
			}
			if (begin2 >= n)
			{
				break;
			}
			if (end2 >= n)
			{
				end2 = n - 1;
			}
			int j = i;
			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;
}
相关推荐
dongf20195 分钟前
R语言KNN算法
算法·数据分析·r语言
小O的算法实验室26 分钟前
2025年IEEE TASE,基于双层耦合平均场博弈的大规模智能体集成任务分配与轨迹规划
人工智能·算法·机器学习
8Qi832 分钟前
LeetCode 337:打家劫舍 III(House Robber III)—— 题解 ✅
算法·leetcode·二叉树·动态规划
地平线开发者32 分钟前
从 INT64 Div 算子约束到 Cast 修复全流程
算法
AI科技星35 分钟前
基于奇合数边界的离散解析数论与双螺旋宇宙本体大统一体系论文全部数学公式汇总表
人工智能·算法·机器学习·架构·学习方法
糖果店的幽灵38 分钟前
Pandas DataFrame 数据结构详解
数据结构·pandas
地平线开发者1 小时前
Horizon 模型多 Batch 配置
算法·自动驾驶
czhaii1 小时前
GB2312简体中文编码表
单片机·算法
8Qi81 小时前
LeetCode 121 & 122:股票买卖问题(DP 对比题解)✅
算法·leetcode·职场和发展·动态规划
一只齐刘海的猫1 小时前
【Leetcode】 接雨水
java·算法·leetcode