S15 排序算法--归并排序

归并排序是一种基于 "分治思想" 的排序算法,核心逻辑是 "将大问题拆解为小问题,解决小问题后合并结果",具有 稳定排序、时间复杂度稳定 等特点,广泛应用于大数据量排序场景(如外部排序)。

核心原理(分治思想)

1. 分(Divide):拆解问题

将待排序数组 递归拆解 为两个长度大致相等的子数组,直到每个子数组仅含 1 个元素。

2. 合(Merge):合并有序子数组

将两个 有序子数组 合并为一个大的有序数组,递归向上合并,最终得到完整的有序数组。

  • 合并核心:需要一个临时数组,通过 "双指针" 比较两个子数组的元素,按从小到大顺序放入临时数组,最后将临时数组拷贝回原数组。

拆解(递归)→ 合并(递归回溯)→ 最终有序数组,本质是 "自顶向下" 的分治过程。

底层细节:为什么要这么设计?

1. 为什么需要临时数组?

  • 若直接在原数组上合并,会覆盖未处理的元素(如合并 [5,9][1,4] 时,原数组位置 0 的5会被1覆盖,导致后续5无法被处理);

  • 临时数组的作用是 "暂存有序结果",合并完成后再拷贝回原数组,保证数据不丢失。

2. 为什么是稳定排序?

  • 稳定排序定义:排序后,相等元素的相对顺序不变 (如 [2, 1, 2] 排序后仍为 [1, 2, 2],两个2的原始顺序保留);

  • 归并排序稳定的关键:合并时用 arr[i] <= arr[j](而非 <)。当左子数组元素等于右子数组元素时,优先选择左子数组的元素,确保相等元素的相对顺序不变。

3. 为什么递归终止条件是left >= right

  • left == right 时,子数组长度为 1(天然有序),无需拆分;

  • left > right 时,子数组为空(无元素可处理),直接返回。

完整实现(C 语言):递归版 + 非递归版

1.合并两个有序数组(Merge)

cpp 复制代码
void Merge(int arr[], int left, int mid, int right) {
	int n1 = mid - left + 1;
	int n2 = right - mid;
	int* brr = (int*)malloc(sizeof(int) * n1);
	int* crr = (int*)malloc(sizeof(int) * n2);
	for (int i = 0; i < n1; i++)brr[i] = arr[i + left];
	for (int r = 0; r < n2; r++)crr[r] = arr[mid + 1 + r];
	int i = 0, j = 0;
	while (i < n1 && j < n2) {
		if (brr[i] > crr[j]) {
			arr[left++] = brr[i++];
		}
		else {
			arr[left++] = crr[j++];
		}
	}
	while (i < n1) {
		arr[left++] = brr[i++];
	}
	while (j < n2) {
		arr[left++] = crr[j++];
	}
}

2.分治拆解问题(Divide)递归版

cpp 复制代码
void Divide1(int arr[], int left, int right) {
	if (left < right) {
		int mid = left + (right - left) / 2;
		Divide1(arr, left, mid);
		Divide1(arr, mid + 1, right);
		Merge(arr, left, mid, right);
	}
}

3.分治拆解问题(Divide)非递归版

cpp 复制代码
//非递归实现
void Divide(int arr[], int left, int right) {
	for (int i = 1; i < right - left + 1; i = i * 2) {
		for (int r = left; r < right - left + 1-i ; r = r + i * 2) {
			int mid = r + i-1;
			int right1 = (r + i * 2-1) < (right - left + 1) ? (r + i * 2-1) : (right - left );
			Merge(arr, r, mid, right1);
		}
	}
}

代码关键说明

  1. 临时数组优化:仅在入口函数申请 1 次临时数组,避免递归中频繁申请释放内存(提升效率);

  2. 稳定排序保证 :合并时用 arr[i] <= arr[j](而非 <),确保相等元素的相对顺序不变;

  3. 溢出避免mid = left + (right - left)/2 替代 (left+right)/2,防止 left+right 超出整数范围;

  4. 递归终止条件left >= right(子数组长度为 1 或 0 时停止拆解)。

时间复杂度与空间复杂度

1. 时间复杂度

  • 最好 / 最坏 / 平均时间复杂度O(n log n)(分治过程的深度为 log n,每层合并的总操作数为 O(n),整体为 n * log n);

  • 稳定性:归并排序是 稳定排序(相等元素的相对顺序不变),适合需要保持原始顺序的场景(如按成绩排序后,保持同成绩学生的录入顺序)。

2. 空间复杂度

  • 递归实现:O(n + log n)n 为临时数组空间,log n 为递归调用栈空间);

  • 非递归实现:O(n)(仅需临时数组空间,无递归栈开销)。

适用场景与注意事项

1. 适用场景

  • 大数据量排序(如百万级、千万级数据);

  • 需稳定排序的场景(如多字段排序:先按成绩排序,再按姓名排序);

  • 外部排序(数据存储在磁盘,无法全部载入内存);

  • 递归深度可控的场景(或用非递归版避免栈溢出)。

2. 注意事项

  • 空间开销:需额外 O(n) 临时数组,内存紧张场景需谨慎;

  • 小规模数据效率:递归版在小规模数据上不如插入排序、快速排序,需结合优化;

  • 递归栈溢出:递归版在 n 极大(如千万级)时可能栈溢出,需用非递归版。

总结

归并排序的核心优势是 时间复杂度稳定(O (n log n))、稳定排序 ,核心劣势是 空间开销较大(O (n))。其分治思想不仅适用于排序,还可延伸到其他问题(如逆序对计数、大规模数据合并)。

相关推荐
智者知已应修善业2 小时前
【c# 想一句话把 List<List<string>>的元素合并成List<string>】2023-2-9
经验分享·笔记·算法·c#·list
B站_计算机毕业设计之家2 小时前
深度学习:python人脸表情识别系统 情绪识别系统 深度学习 神经网络CNN算法 ✅
python·深度学习·神经网络·算法·yolo·机器学习·cnn
星释3 小时前
Rust 练习册 :Luhn Trait与Trait实现
网络·算法·rust
ゞ 正在缓冲99%…3 小时前
leetcode1770.执行乘法运算的最大分数
java·数据结构·算法·动态规划
abcefg_h3 小时前
链表算法---基本算法操作(go语言版)
算法·链表·golang
小O的算法实验室3 小时前
2022年IEEE TITS SCI2区TOP,基于切线交点和目标引导策略的无人机自主路径规划,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
Mr_Oak4 小时前
【multi-model】moco系列&SimCLR&BEiT
人工智能·深度学习·神经网络·算法·计算机视觉·transformer·对比学习
尼古拉斯·纯情暖男·天真·阿玮4 小时前
动态规划——子序列问题
java·算法·动态规划
耳总是一颗苹果4 小时前
数据结构---时空复杂度
数据结构