深入浅出:C++手搓归并排序——从“分治”哲学到工业级泛型实现

引言:当“排队”遇上“分而治之”

想象一下,你手里有一副洗乱的扑克牌,要求你在最短的时间内把 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)

当左右两半各自有序后,启动归并引擎:

  • 用两根指针 ij 分别扫描左右半区;
  • 比较 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:模板技巧与易错点
  1. 特化与重载:
    用函数重载而非模板特化,省去 template<> 的冗长,对数组和 vector 提供统一命名。
  2. 比较器硬编码:
    当前实现只支持 operator<=,若需自定义排序规则,可再增加一个 Compare comp 模板形参,与 STL 风格对齐。
  3. 异常安全:
    数组版本若 new 抛出 std::bad_alloc,会直接向上传播,调用方可用 try/catch 兜底;vector 版本利用 RAII,自动释放已分配内存。
  4. 深拷贝 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];
        }
    }
};
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值