← 目录 / 算法文档 · 模块九 分治排序 / 9.2 归并排序

9.2 归并排序

先把数组拆到只剩一个数(天然有序),再两两合并——合并两个有序数组,比想象中简单得多。

本页目录
① 什么是归并排序

如果有两个已经排好序的数组,把它们合并成一个更大的有序数组,其实是一件很简单的事——完全不需要重新排序,只要拿两根手指分别指着两个数组的开头,每次比较手指指到的两个数,把较小的那个拿出来放进新数组,对应的手指往后挪一位,一直重复,直到其中一个数组被"掏空",另一个数组剩下的部分直接整体接到后面就行了。

归并排序的思路,就是把这个"合并两个有序数组"的简单操作反复利用起来:先把原数组一路拆成一个个只有 1 个元素的小段——只有 1 个元素当然天然有序——再把相邻的小段两两合并成有序的大段,合并出来的大段再两两合并……一层一层往上合,直到合并成整个有序数组。

② 核心操作:合并两个有序数组

在学拆分之前,先把最核心的"合并"操作单独看清楚。假设已经有两个排好序的小数组 [2, 5, 8][1, 3, 9],要把它们合并成一个有序数组:

双指针合并两个有序数组
初始:两个指针都指向各自数组的开头,比较 2 和 1 —— 1 更小
2
↑ i
5
8
1
↑ j
3
9
1
1 被取走,j 指针后移;比较 2 和 3 —— 2 更小
2
↑ i
5
8
1
3
↑ j
9
1
2
2 被取走,i 指针后移;比较 5 和 3 —— 3 更小
2
5
↑ i
8
1
3
↑ j
9
1
2
3
3 被取走,右边数组用完了 j 越界 —— 左边剩下的 5、8 直接整体接上去,不用再比较
2
5
8
1
3
9
1
2
3
5
8
左边剩下的 5、8 整体接上去之后,别忘了右边其实还有一个 9 尚未处理——只是这一步右边恰好先用完了,所以看不到它被"接上"的过程。完整走完的话,最终结果应该是 1 2 3 5 8 9。这里想强调的是:不管是哪一边先用完,另一边剩下的所有元素都必须原样整体接到结果末尾,一个都不能漏,下面的代码和陷阱里会再强调一次。
💡
合并操作的关键:因为两个小数组各自已经是有序的,所以每次只需要比较两个指针当前指到的数,把较小的拿走,被拿走的那一边指针后移——不需要回头比较,一路往前走一遍就能合并完,是线性的 O(n) 操作。这也是归并排序效率的关键所在。
③ 递归实现:先拆到底,再逐层合并

有了"合并"这个工具,剩下的问题就是:怎么才能拿到两个"已经排好序"的小数组?答案是递归地拆——不断把数组从中间切成两半,直到每一段都只剩 1 个元素(1 个元素当然已经是"有序"的),再把这些最小的段两两合并、逐层往上,最终合并回整个数组。用数组 [5, 2, 8, 1, 9, 3] 演示完整过程:

先一路拆分,再逐层合并
原数组
5
2
8
1
9
3
拆成两半
5
2
8
1
9
3
继续拆,直到每段只剩 1 个元素(天然有序,不用再拆)
5
2
8
1
9
3
两两合并(用②的双指针方法),每一小段内部变得有序
5
2
8
1
9
3
再合并成两个更大的有序段
2
5
8
1
3
9
最后一次合并,得到完整的有序数组
1
2
3
5
8
9
"拆"的过程只是不断地对半切,不做任何实际工作;真正把数排好序的,是"合"的过程——每一层合并都用②节的双指针方法,逐层把小的有序段拼成大的有序段。6 个数只需要拆 3 层(log₂6 ≈ 2.6,向上取整是 3 层),每层合并的总工作量都是 O(n),所以整体是 O(n log n)
C++ · 归并排序完整实现
1int temp[100005]; // 合并时用的临时数组,大小要开够
2
3void Merge(int arr[], int left, int mid, int right)
4{
5 int i = left, j = mid + 1, k = left; // i管左段,j管右段,k管临时数组的位置
6
7 while (i <= mid && j <= right) // 两段都还有元素没比完
8 {
9 if (arr[i] <= arr[j]) { temp[k++] = arr[i++]; } // 左边小,拿左边
10 else { temp[k++] = arr[j++]; } // 右边小,拿右边
11 }
12
13 while (i <= mid) { temp[k++] = arr[i++]; } // 左段还剩的,整体接上(不能漏!)
14 while (j <= right) { temp[k++] = arr[j++]; } // 右段还剩的,整体接上(不能漏!)
15
16 for (int x = left; x <= right; x++)
17 {
18 arr[x] = temp[x]; // 把排好的结果从临时数组写回原数组
19 }
20}
21
22void MergeSort(int arr[], int left, int right)
23{
24 if (left >= right) return; // 递归终止:只剩 0 或 1 个元素,天然有序
25
26 int mid = (left + right) / 2; // 从中间切一刀
27 MergeSort(arr, left, mid); // 递归排左半段
28 MergeSort(arr, mid + 1, right); // 递归排右半段
29 Merge(arr, left, mid, right); // 左右两段各自排好后,合并起来
30}
31
32// 调用方式:MergeSort(arr, 0, n - 1);
📖
递归的两层含义:MergeSort 自己调用自己,把"排序整个区间"拆成"排序左半区间"和"排序右半区间"两个规模更小、结构完全相同的子问题——这正是递归的核心特征。递归到 left >= right(区间只剩 0 或 1 个元素)时停下来,因为 1 个元素不需要排序,直接就是有序的。
④ 完整代码与复杂度
情况时间复杂度说明
最好情况O(n log n)归并排序不会因为数据"本来就有序"而变快
平均情况O(n log n)拆分固定是 log n 层,每层合并固定是 O(n)
最坏情况O(n log n)三种情况复杂度完全相同,非常稳定
🎯
归并排序最大的特点是无论数据长什么样,复杂度永远是 O(n log n)——不像冒泡排序,遇到已经排好的数据会变快,遇到完全逆序的数据会变成最坏的 O(n²)。这种"旱涝保收"的稳定性是归并排序的优势,代价是需要额外开一个和原数组一样大的临时数组(O(n) 的额外空间),不是"原地排序"。另外,归并排序在相等元素之间不会交换相对顺序(比较时写的是 arr[i] <= arr[j],左边相等时优先取左边),这个性质叫"稳定排序",在某些需要保持原始顺序的场景里很重要。这一节先重点吃透归并排序;同一模块的快速排序会在后面单独补上,两者都是"分治"思想的经典应用,但拆分和合并的方式正好相反。