引言:当“排队”遇上“分而治之”
想象一下,你手里有一副洗乱的扑克牌,要求你在最短的时间内把 52 张牌按点数从小到大排好。
最直觉的做法是“插空”——一张一张找位置,这就是插入排序;但当牌量上升到 10 万、100 万时,插空就显得笨拙了。
归并排序(Merge Sort)给出的思路截然不同:
把牌堆不断对半分,直到每人只剩一张(天然有序),再两两合并,合并时按顺序捋一遍,最后整副牌就自然有序了。
这种“先分再合”的策略,正式名称叫 分治法(Divide & Conquer),是计算机科学里最具美感的通用范式之一。
从 1945 年 John von Neumann 在 EDVAC 上写下第一份归并排序代码,到今天 Linux 内核的 stable_sort,再到 C++ STL 的 std::inplace_merge,归并排序始终是“稳定 + 最坏 O(n log n)”这一黄金组合的代言人。
本文,我们将用一份工业级 C++17 泛型实现(支持原生数组与 std::vector)做主线,带你拆解归并排序的里里外外:
- 先聊“分治”哲学与算法骨架;
- 再逐行精读代码,看清每一次内存分配、边界判断与模板推导;
- 接着算账:时间、空间、缓存、分支预测;
- 最后给出优势、陷阱与实战扩展。
读完,你不仅能随手写出一份“面试官挑不出刺”的归并排序,更能把“分治”思想迁移到海量场景——从外部排序到并行计算,从逆序数统计到链表排序。
算法原理详解:三张图读懂“分、治、合”
1. 分(Divide)
把区间 [left, right] 以中点 mid 劈成两半,递归下去,直到子区间长度 ≤1。
mergeSort(A, left, right):
if left >= right: return
mid = (left + right) / 2
mergeSort(A, left, mid) # 左半边
mergeSort(A, mid+1, right) # 右半边
2. 治(Conquer)+ 合(Combine)
当左右两半各自有序后,启动归并引擎:
- 用两根指针
i、j分别扫描左右半区; - 比较
A[i]与A[j],将小者放入临时数组temp,指针前进; - 任一子数组耗尽后,把剩余元素直接搬过去;
- 最后把
temp拷贝回原数组,完成一次“局部有序 → 全局有序”的升华。
merge(A, left, mid, right, temp):
i = left, j = mid+1, k = left
while i <= mid and j <= right:
if A[i] <= A[j]: temp[k++] = A[i++]
else: temp[k++] = A[j++]
while i <= mid: temp[k++] = A[i++]
while j <= right: temp[k++] = A[j++]
for idx from left to right: A[idx] = temp[idx]
3. 递归树全景
若把递归调用画成二叉树:
- 树高
⌈log₂n⌉; - 每层合并总代价
O(n); - 于是总时间
O(n log n),与输入数据分布无关——最坏也是 O(n log n),这是归并排序最迷人的安全牌。
C++实现逐行解析:一份模板搞定数组与 vector
下面把 MergeSort.hpp 拆成 4 个逻辑块,逐行讲清“为什么这么写”。
块 1:公有门面——sort / printArray / printVector
template<typename T>
static void sort(T arr[], int size);
static无状态,工具类无需实例化;- 模板形参
T只需支持operator<=即可,对 POD、自定义类型一视同仁; - 先做一次
size <= 1快速返回,避免后续new的无谓开销。
T* temp = new T[size];
- 一次性分配 整段临时缓冲区,而非每次递归都
new; - 归并排序的“最大空间痛点”就集中在这
n个元素,后续递归只传引用,不再二次分配。
std::vector<T> temp(vec.size());
- 对于
std::vector重载,同样预分配一次,利用vector的 RAII 自动释放,异常安全。
块 2:私有递归引擎——mergeSort
int mid = left + (right - left) / 2;
- 防溢出写法,比
(left+right)/2更安全; - 整数除法向下取整,保证
mid严格小于right,左右区间不会重叠或遗漏。
递归终止条件:
if (left < right) { ... }
- 只有区间长度 ≥2 才继续分,==1 时天然有序,直接回溯。
块 3:归并核心——merge
while (i <= mid && j <= right)
- 双指针扫描,稳定性关键:当
vec[i] == vec[j]时先取左半部分,相等元素的原始相对次序被打包进temp,再原样写回。 - 因此归并排序是 稳定排序,这也是
std::stable_sort的底层候选。
边界收尾:
while (i <= mid) temp[k++] = vec[i++];
while (j <= right) temp[k++] = vec[j++];
- 无需额外判断,左右剩余区间必定有序,直接批量搬。
最后拷贝:
for (int idx = left; idx <= right; ++idx) vec[idx] = temp[idx];
- 一次局部拷贝,把
temp的“快照”写回原区间; - 注意 只覆盖当前正在合并的区间,不会污染其他层递归的数据。
块 4:模板技巧与易错点
- 特化与重载:
用函数重载而非模板特化,省去template<>的冗长,对数组和vector提供统一命名。 - 比较器硬编码:
当前实现只支持operator<=,若需自定义排序规则,可再增加一个Compare comp模板形参,与 STL 风格对齐。 - 异常安全:
数组版本若new抛出std::bad_alloc,会直接向上传播,调用方可用try/catch兜底;vector版本利用 RAII,自动释放已分配内存。 - 深拷贝 vs 移动:
对于昂贵的大对象(如std::string),建议把temp[k] = vec[i]换成temp[k] = std::move(vec[i])并配合noexcept移动语义,可削减 50%+ 拷贝开销。
复杂度分析:把账算到 cache 行
| 场景 | 时间复杂度 | 说明 |
|---|---|---|
| 最好 | Θ(n log n) | 哪怕输入已有序,也要完整拆分合并 |
| 平均 | Θ(n log n) | 递归树高 log₂n,每层线性扫描 |
| 最坏 | Θ(n log n) | 与平均一致,无最坏退化 |
空间复杂度:
- 辅助数组
T[n]:Θ(n) - 递归栈:树高
⌈log₂n⌉,每帧常数开销,合计 Θ(log n) - 总空间 Θ(n + log n) = Θ(n)
缓存友好度:
归并排序对缓存并不友好——每次合并都要在 temp 与原数组间来回拷贝,跨度大时容易触发 cache miss。
工业级优化(如 std::stable_sort)在 元素 ≤ 32 字节 时会切换到 内省插入排序,并把临时缓冲区拆成 块大小 ≤ 256 的局部块,以提升局部性。读者可据此做二次改造。
优势与局限性:一张对照表
| 优势 | 局限性 |
|---|---|
| 稳定排序,相等元素相对次序不变 | 需要额外 Θ(n) 空间(数组版) |
| 最坏时间复杂度仍是 O(n log n),可预测性强 | 对小型数组常数因子大,比插入/选择排序慢 |
| 天然 并行可分割:左右子区间可独立排序 | 递归深度 O(log n),对栈空间敏感的嵌入式环境需改迭代 |
可外部排序:磁盘多路归并(如 Linux sort -m) | 对链表版本需要改写指针合并逻辑,缓存局部性进一步下降 |
至此,归并排序的“分治”哲学已融化在你的 C++ 工具箱里。下一次,无论是对 1 亿个 double 做外部排序,还是在 GPU 上并行归并,你都能从这份模板出发,游刃有余。 Happy coding!
#include <iostream>
#include <vector>
#include <algorithm>
/**
* @class MergeSort
* @brief 实现归并排序算法的类
*
* 这个类提供了使用归并排序算法对数组进行排序的方法,
* 归并排序是一种分治算法,时间复杂度为 O(n log n)。
* 分:将待排序的数组不断二分,直到每个子数组只有一个元素(或为空),此时认为它们已经有序。
* 治:将两个有序的子数组合并成一个更大的有序数组,这个过程称为“归并”。
* 合:持续归并相邻的有序子数组,直到整个数组有序。
*/
class MergeSort {
public:
/**
* @brief 使用归并排序算法对数组进行排序
* @tparam T 数组中元素的类型(必须支持比较运算符)
* @param arr 要排序的数组
* @param size 数组的大小
*/
template<typename T>
static void sort(T arr[], int size) {
if (size <= 1) return;
// 创建临时数组用于合并
T* temp = new T[size];
mergeSort(arr, 0, size - 1, temp);
delete[] temp;
}
/**
* @brief 使用归并排序算法对向量进行排序
* @tparam T 向量中元素的类型(必须支持比较运算符)
* @param vec 要排序的向量
*/
template<typename T>
static void sort(std::vector<T>& vec) {
if (vec.size() <= 1) return;
// 创建临时向量用于合并
std::vector<T> temp(vec.size());
mergeSort(vec, 0, vec.size() - 1, temp);
}
/**
* @brief 打印数组的元素
* @tparam T 数组中元素的类型
* @param arr 要打印的数组
* @param size 数组的大小
*/
template<typename T>
static void printArray(const T arr[], int size) {
for (int i = 0; i < size; i++) {
std::cout << arr[i];
if (i < size - 1) std::cout << ", ";
}
std::cout << std::endl;
}
/**
* @brief 打印向量的元素
* @tparam T 向量中元素的类型
* @param vec 要打印的向量
*/
template<typename T>
static void printVector(const std::vector<T>& vec) {
for (size_t i = 0; i < vec.size(); i++) {
std::cout << vec[i];
if (i < vec.size() - 1) std::cout << ", ";
}
std::cout << std::endl;
}
private:
/**
* @brief 数组的递归归并排序实现
* @tparam T 元素的类型
* @param arr 要排序的数组
* @param left 子数组的左索引
* @param right 子数组的右索引
* @param temp 用于合并的临时数组
*/
template<typename T>
static void mergeSort(T arr[], int left, int right, T temp[]) {
if (left < right) {
int mid = left + (right - left) / 2;
// 排序前半部分和后半部分
mergeSort(arr, left, mid, temp);
mergeSort(arr, mid + 1, right, temp);
// 合并已排序的两部分
merge(arr, left, mid, right, temp);
}
}
/**
* @brief 向量的递归归并排序实现
* @tparam T 元素的类型
* @param vec 要排序的向量
* @param left 子向量的左索引
* @param right 子向量的右索引
* @param temp 用于合并的临时向量
*/
template<typename T>
static void mergeSort(std::vector<T>& vec, int left, int right, std::vector<T>& temp) {
if (left < right) {
int mid = left + (right - left) / 2;
// 排序前半部分和后半部分
mergeSort(vec, left, mid, temp);
mergeSort(vec, mid + 1, right, temp);
// 合并已排序的两部分
merge(vec, left, mid, right, temp);
}
}
/**
* @brief 合并数组的两个已排序子数组
* @tparam T 元素的类型
* @param arr 包含要合并子数组的数组
* @param left 左索引
* @param mid 中间索引
* @param right 右索引
* @param temp 用于合并的临时数组
*/
template<typename T>
static void merge(T arr[], int left, int mid, int right, T temp[]) {
int i = left; // 第一个子数组的初始索引
int j = mid + 1; // 第二个子数组的初始索引
int k = left; // 合并子数组的初始索引
// 复制数据到临时数组
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k] = arr[i];
i++;
} else {
temp[k] = arr[j];
j++;
}
k++;
}
// 复制左子数组的剩余元素(如果有)
while (i <= mid) {
temp[k] = arr[i];
i++;
k++;
}
// 复制右子数组的剩余元素(如果有)
while (j <= right) {
temp[k] = arr[j];
j++;
k++;
}
// 将合并后的元素复制回原始数组
for (int idx = left; idx <= right; idx++) {
arr[idx] = temp[idx];
}
}
/**
* @brief 合并向量的两个已排序子向量
* @tparam T 元素的类型
* @param vec 包含要合并子向量的向量
* @param left 左索引
* @param mid 中间索引
* @param right 右索引
* @param temp 用于合并的临时向量
*/
template<typename T>
static void merge(std::vector<T>& vec, int left, int mid, int right, std::vector<T>& temp) {
int i = left; // 第一个子向量的初始索引
int j = mid + 1; // 第二个子向量的初始索引
int k = left; // 合并子向量的初始索引
// 复制数据到临时向量
while (i <= mid && j <= right) {
if (vec[i] <= vec[j]) {
temp[k] = vec[i];
i++;
} else {
temp[k] = vec[j];
j++;
}
k++;
}
// 复制左子向量的剩余元素(如果有)
while (i <= mid) {
temp[k] = vec[i];
i++;
k++;
}
// 复制右子向量的剩余元素(如果有)
while (j <= right) {
temp[k] = vec[j];
j++;
k++;
}
// 将合并后的元素复制回原始向量
for (int idx = left; idx <= right; idx++) {
vec[idx] = temp[idx];
}
}
};
943

被折叠的 条评论
为什么被折叠?



