归并排序

归并排序 (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:

  1. 分解:将数组从中间分成两个子数组
  2. 解决:递归地对两个子数组进行排序
  3. 合并:将两个有序的子数组合并成一个有序数组

核心优势稳定性适合外部排序是归并排序的最大特点^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] 如何优化归并排序?

  1. 小数组优化:当 n < 16 时切换到插入排序
  2. 避免重复创建:复用临时数组
  3. 并行归并:利用多核 CPU 并行处理子数组

相关算法

排序算法对比

  • [[快速排序]] - 同样的 $O(n \log n)$ 时间复杂度,但不稳定
  • [[堆排序|堆排序(原地排序)]] - 原地排序,但不稳定
  • [[插入排序#算法优化|插入排序]] - 小规模数据效率高
  • [[计数排序]] - 线性时间排序,但要求特定范围
  • [[TimSort]] - Python 和 Java 的默认排序算法,结合了归并和插入排序

核心概念

  • [[递归算法]] - 归并排序的基础
  • [[分治法基础#经典应用|分治法]] - 归并排序的核心思想
  • [[二叉树#遍历|二叉树遍历]] - 归并排序的递归树结构

应用扩展

  • [[逆序对问题]] - 归并排序的经典应用
  • [[外部排序]] - 处理大规模数据
  • [[线段树]] - 归并思想的高级应用

学习路径

推荐学习顺序

  1. 先掌握 [[插入排序]]
  2. 理解 [[递归算法]]
  3. 学习 [[归并排序]](当前)
  4. 进阶 [[快速排序]]
  5. 拓展 [[线段树]]

习题推荐

基础练习

    • 相关代码:[[#C++ 实现]]

进阶练习

    • 相关知识点:[[归并排序#合并过程]] ^merge-process
    • 练习平台:
    • 提示:在合并时统计逆序对
    • 在合并时,左边元素比右边小时,累加小和

高级应用

    • 场景:100GB 数据文件排序(内存只有 1GB)
    • 提示:多路归并 + 败者树
    • 小数组(n < 16)切换到插入排序
    • 对已有序数组提前终止

LeetCode 练习

竞赛题目

总结

特性 归并排序
时间复杂度 $O(n \log n)$
空间复杂度 $O(n)$
稳定性 ✅ 稳定
是否原地排序 ❌ 非原地
适用场景 大规模数据、外部排序

学习技巧

Obsidian 使用提示

  • Ctrl + Click 跟踪笔记中的链接
  • Ctrl + E 切换编辑/预览模式
  • Ctrl + G 打开关系图谱,查看算法关联

算法学习建议

  1. 理解核心思想:掌握分治法的三步骤 [[#算法原理]]
  2. 手写实现:至少熟练掌握一种语言 [[#代码实现]]
  3. 分析复杂度:理解为什么是 $O(n \log n)$ [[#复杂度分析]]
  4. 刷题实践:从逆序对问题入手 [[#习题推荐]]

最后更新:2026-03-03

参考文献


  1. John von Neumann 在 1945 年为 EDVAC 计算机开发的排序程序 ↩︎

posted @ 2026-03-03 15:41  stephen_zuo  阅读(84)  评论(0)    收藏  举报