归并排序
归并排序 (Merge Sort)
算法概述
[!info] 核心思想
归并排序是一种基于分治法(Divide and Conquer)的排序算法。它将数组分成两半,递归地对每半进行排序,然后将排序好的两半合并在一起。
[!abstract] 算法分类
归并排序属于比较排序算法,时间复杂度下界为 $\Omega(n \log n)$。它是稳定排序的典型代表,由 John von Neumann 于 1945 年发明[1]。
[!example] 归并过程示例
合并[2, 5, 8]和[1, 3, 6, 9]的过程:
- 比较 2 和 1 → 取 1,剩余
[2,5,8]和[3,6,9]- 比较 2 和 3 → 取 2,剩余
[5,8]和[3,6,9]- 比较 5 和 3 → 取 3,剩余
[5,8]和[6,9]- 比较 5 和 6 → 取 5,剩余
[8]和[6,9]- 比较 8 和 6 → 取 6,剩余
[8]和[9]- 比较 8 和 9 → 取 8,剩余
[]和[9]- 复制剩余 9
- 最终结果:
[1, 2, 3, 5, 6, 8, 9]
算法原理
归并排序分为三个步骤 ^algorithm-steps:
- 分解:将数组从中间分成两个子数组
- 解决:递归地对两个子数组进行排序
- 合并:将两个有序的子数组合并成一个有序数组
核心优势:稳定性和适合外部排序是归并排序的最大特点^key-advantages
可视化流程
graph TD
A[原始数组: 38, 27, 43, 3, 9, 82, 10] --> B[分成两半]
B --> C[左半: 38, 27, 43, 3]
B --> D[右半: 9, 82, 10]
C --> E[继续分割]
D --> F[继续分割]
E --> G[递归排序]
F --> H[递归排序]
G --> I[合并: 3, 27, 38, 43]
H --> J[合并: 9, 10, 82]
I --> K[最终合并]
J --> K
K --> L[结果: 3, 9, 10, 27, 38, 43, 82]
style A fill:#ffe6e6
style L fill:#e6ffe6
style K fill:#e6f3ff
代码实现
C++ 实现
#include <vector>
using namespace std;
// 合并两个有序子数组
void merge(vector<int>& arr, int left, int mid, int right) {
// 创建临时数组存储合并结果
vector<int> temp(right - left + 1);
int i = left; // 左子数组起始索引
int j = mid + 1; // 右子数组起始索引
int k = 0; // 临时数组索引
// 比较并合并
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 复制左子数组剩余元素
while (i <= mid) {
temp[k++] = arr[i++];
}
// 复制右子数组剩余元素
while (j <= right) {
temp[k++] = arr[j++];
}
// 将临时数组复制回原数组
for (i = left, k = 0; i <= right; i++, k++) {
arr[i] = temp[k];
}
}
// 归并排序主函数
void mergeSort(vector<int>& arr, int left, int right) {
if (left < right) {
// 找到中间点
int mid = left + (right - left) / 2;
// 递归排序左右两半
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
// 合并已排序的两半
merge(arr, left, mid, right);
}
}
// 使用示例
int main() {
vector<int> arr = {38, 27, 43, 3, 9, 82, 10};
mergeSort(arr, 0, arr.size() - 1);
return 0;
}
Python 实现
def merge(arr, left, mid, right):
"""合并两个有序子数组"""
temp = []
i, j = left, mid + 1
# 比较并合并
while i <= mid and j <= right:
if arr[i] <= arr[j]:
temp.append(arr[i])
i += 1
else:
temp.append(arr[j])
j += 1
# 复制剩余元素
temp.extend(arr[i:mid + 1])
temp.extend(arr[j:right + 1])
# 将结果复制回原数组
arr[left:right + 1] = temp
def merge_sort(arr, left, right):
"""归并排序主函数"""
if left < right:
mid = (left + right) // 2
merge_sort(arr, left, mid)
merge_sort(arr, mid + 1, right)
merge(arr, left, mid, right)
# 使用示例
arr = [38, 27, 43, 3, 9, 82, 10]
merge_sort(arr, 0, len(arr) - 1)
print(arr) # [3, 9, 10, 27, 38, 43, 82]
复杂度分析
时间复杂度
归并排序的时间复杂度为 $O(n \log n)$
推导过程:
- 每次分解将数组分成两半,共需要 == $\log n$ == 层
- 每层需要合并所有元素,共 $O(n)$ 时间
- 总时间复杂度:$T(n) = 2T(n/2) + O(n) =$ $O(n \log n)$
关键:无论最好、最坏还是平均情况,归并排序的时间复杂度都是 $O(n \log n)$,这使其在性能要求稳定的场景非常有用^stable-performance。
空间复杂度
空间复杂度为 $O(n)$
- 需要额外的临时数组来存储合并结果
- 递归调用栈的深度为 $O(\log n)$
- 主要空间消耗来自临时数组,而非递归栈
复杂度对比表
| 场景 | 时间复杂度 | 说明 |
|---|---|---|
| 最坏情况 | $O(n \log n)$ | 数组完全逆序 |
| 平均情况 | $O(n \log n)$ | 随机排列 |
| 最好情况 | $O(n \log n)$ | 数组已有序 |
| 空间复杂度 | $O(n)$ | 需要额外空间 |
⚡ 算法特性
[!success] 稳定排序
归并排序是稳定的排序算法,即相等元素的相对顺序在排序后保持不变。
[!tip] 适用场景
- 大规模数据排序
- 外部排序(数据无法全部装入内存)
- 链表排序(空间复杂度可优化到 $O(1)$)
[!warning] 注意事项
- 不是原地排序,需要 $O(n)$ 额外空间
- 对于小规模数据,常数因子较大的排序算法(如插入排序)可能更快
- 不适合对空间复杂度要求严格的场景
[!faq]- 常见问题
[!question] 为什么归并排序是稳定的?
因为在合并时,当两个元素相等时,优先取左边的元素,保持相对顺序。在代码中体现为if (arr[i] <= arr[j])的<=判断。[!question] 递归和迭代实现哪个更好?
- 递归:代码简洁直观,易于理解
- 迭代:避免递归栈溢出,空间效率略高
- 建议根据数据规模选择,大规模数据优先迭代
[!question] 如何优化归并排序?
- 小数组优化:当 n < 16 时切换到插入排序
- 避免重复创建:复用临时数组
- 并行归并:利用多核 CPU 并行处理子数组
相关算法
排序算法对比
- [[快速排序]] - 同样的 $O(n \log n)$ 时间复杂度,但不稳定
- [[堆排序|堆排序(原地排序)]] - 原地排序,但不稳定
- [[插入排序#算法优化|插入排序]] - 小规模数据效率高
- [[计数排序]] - 线性时间排序,但要求特定范围
- [[TimSort]] - Python 和 Java 的默认排序算法,结合了归并和插入排序
核心概念
- [[递归算法]] - 归并排序的基础
- [[分治法基础#经典应用|分治法]] - 归并排序的核心思想
- [[二叉树#遍历|二叉树遍历]] - 归并排序的递归树结构
应用扩展
- [[逆序对问题]] - 归并排序的经典应用
- [[外部排序]] - 处理大规模数据
- [[线段树]] - 归并思想的高级应用
学习路径
推荐学习顺序:
- 先掌握 [[插入排序]]
- 理解 [[递归算法]]
- 学习 [[归并排序]](当前)
- 进阶 [[快速排序]]
- 拓展 [[线段树]]
习题推荐
基础练习
-
- 相关代码:[[#C++ 实现]]
进阶练习
-
- 相关知识点:[[归并排序#合并过程]] ^merge-process
- 练习平台:
- 洛谷 P1908
普及+/提高 - AcWing 788
- 洛谷 P1908
- 提示:在合并时统计逆序对
-
- 在合并时,左边元素比右边小时,累加小和
高级应用
-
- 场景:100GB 数据文件排序(内存只有 1GB)
- 提示:多路归并 + 败者树
-
- 小数组(n < 16)切换到插入排序
- 对已有序数组提前终止
LeetCode 练习
竞赛题目
总结
| 特性 | 归并排序 |
|---|---|
| 时间复杂度 | $O(n \log n)$ |
| 空间复杂度 | $O(n)$ |
| 稳定性 | ✅ 稳定 |
| 是否原地排序 | ❌ 非原地 |
| 适用场景 | 大规模数据、外部排序 |
学习技巧
Obsidian 使用提示
- Ctrl + Click 跟踪笔记中的链接
- Ctrl + E 切换编辑/预览模式
- Ctrl + G 打开关系图谱,查看算法关联
算法学习建议
- 理解核心思想:掌握分治法的三步骤 [[#算法原理]]
- 手写实现:至少熟练掌握一种语言 [[#代码实现]]
- 分析复杂度:理解为什么是 $O(n \log n)$ [[#复杂度分析]]
- 刷题实践:从逆序对问题入手 [[#习题推荐]]
最后更新:2026-03-03
参考文献
John von Neumann 在 1945 年为 EDVAC 计算机开发的排序程序 ↩︎

浙公网安备 33010602011771号