← Back

Merge Sort 归并排序


Merge Sort 归并排序

核心结论

**归并排序(Merge Sort)**是一种典型的分治算法:将序列递归地划分为两个子序列,分别排序后,再在线性时间内合并。

  • 最好、平均、最坏时间复杂度均为 Θ(nlogn)\Theta(n\log n)
  • 标准数组实现的辅助空间复杂度为 Θ(n)\Theta(n)
  • 可以实现为稳定排序
  • 标准数组实现通常不是原地排序

1. 问题定义

给定包含 nn 个元素的序列:

A=[a0,a1,,an1]A=[a_0,a_1,\ldots,a_{n-1}]

将其重新排列为:

A=[a0,a1,,an1]A'=[a'_0,a'_1,\ldots,a'_{n-1}]

使得:

a0a1an1a'_0\leq a'_1\leq\cdots\leq a'_{n-1}

归并排序属于比较排序,排序依据仅来自元素之间的比较结果。

2. 基本思想

归并排序采用分治策略,将一个规模较大的排序问题分解为规模更小、结构相同的子问题。

2.1 分治三步

  1. 分解(Divide)

    • 将当前区间从中点划分为左右两个子区间。
  2. 解决(Conquer)

    • 递归地对左右子区间进行归并排序。
    • 当区间中至多包含一个元素时,该区间天然有序。
  3. 合并(Combine)

    • 将两个已经有序的子区间合并为一个完整的有序区间。
    • 合并两个总长度为 nn 的有序序列需要 Θ(n)\Theta(n) 时间。

关键前提

MERGE 操作接收的两个子序列必须已经分别有序。
归并排序的递归过程保证了这一前提。

2.2 递归关系

忽略向上取整和向下取整后,归并排序的运行时间满足:

T(n)=2T(n2)+Θ(n)T(n)=2T\left(\frac{n}{2}\right)+\Theta(n)

其中:

  • 2T(n/2)2T(n/2):递归排序两个规模约为 n/2n/2 的子序列;
  • Θ(n)\Theta(n):合并两个有序子序列。

由主定理可得:

T(n)=Θ(nlogn)T(n)=\Theta(n\log n)

3. 具体过程

本文统一使用 0-based 下标左闭右开区间

A[left:right)A[\text{left}:\text{right})

区间包含 left,但不包含 right,其长度为:

rightleft\text{right}-\text{left}

对区间 A[left:right) 执行归并排序:

  1. right - left <= 1,直接返回;

  2. 计算中点:

    mid=left+rightleft2\text{mid} = \text{left} + \left\lfloor \frac{\text{right}-\text{left}}{2} \right\rfloor
  3. 递归排序左半部分 A[left:mid)

  4. 递归排序右半部分 A[mid:right)

  5. 合并两个有序区间:

    • A[left:mid)
    • A[mid:right)

3.1 合并两个有序区间

设两个有序序列分别为:

L=[l0,l1,,lp1]L=[l_0,l_1,\ldots,l_{p-1}] R=[r0,r1,,rq1]R=[r_0,r_1,\ldots,r_{q-1}]

使用两个指针 ij

  • i 指向 L 中尚未合并的第一个元素;
  • j 指向 R 中尚未合并的第一个元素。

每次比较 L[i]R[j]

  • L[i] <= R[j],将 L[i] 放入结果;
  • 否则,将 R[j] 放入结果;
  • 当其中一个序列耗尽后,将另一个序列的剩余元素依次追加到结果末尾。

如何保证稳定性

当两个元素的关键字相等时,优先取左侧子序列中的元素,即使用:

if L[i] <= R[j]

而不是只在 L[i] < R[j] 时取左侧元素。这样可以保留相等元素原有的相对次序。

4. 伪代码

4.1 归并排序

MERGE-SORT(A, left, right)
    // 排序区间 A[left:right)
    if right - left <= 1
        return

    mid <- left + floor((right - left) / 2)

    MERGE-SORT(A, left, mid)
    MERGE-SORT(A, mid, right)
    MERGE(A, left, mid, right)

初始调用:

MERGE-SORT(A, 0, length(A))

4.2 合并操作

MERGE(A, left, mid, right)
    // A[left:mid) 和 A[mid:right) 已经分别有序

    temp <- empty array
    i <- left
    j <- mid

    while i < mid and j < right
        if A[i] <= A[j]
            APPEND(temp, A[i])
            i <- i + 1
        else
            APPEND(temp, A[j])
            j <- j + 1

    while i < mid
        APPEND(temp, A[i])
        i <- i + 1

    while j < right
        APPEND(temp, A[j])
        j <- j + 1

    for k <- 0 to length(temp) - 1
        A[left + k] <- temp[k]

5. 正确性说明

归并排序的正确性可以分为 MERGE 的正确性和递归算法的正确性。

5.1 MERGE 的循环不变式

在主循环每次迭代开始时,temp 满足:

  1. temp 已按非递减顺序排列;
  2. temp 包含左右子序列中已经处理的全部元素;
  3. temp 中的元素是两个子序列全部元素中最小的若干个元素;
  4. 指针 ij 分别指向左右子序列尚未处理的最小元素。

由于每次都从 A[i]A[j] 中选取较小者,因此不变式在每次迭代后仍然成立。

当某个子序列耗尽时,另一个子序列的剩余部分本身已有序,并且其元素均不小于 temp 中最后一个元素,因此直接追加仍可得到有序结果。

5.2 递归正确性

对区间长度 nn 使用数学归纳法。

基础情况:

n1n\leq 1 时,区间为空或仅含一个元素,天然有序。

归纳假设:

假设归并排序能够正确排序所有长度小于 nn 的区间。

归纳步骤:

对于长度为 nn 的区间:

  1. 左右子区间长度均小于 nn
  2. 根据归纳假设,递归调用后两个子区间分别有序;
  3. 根据 MERGE 的正确性,两个有序子区间能够被正确合并为一个有序区间。

因此,归并排序能够正确排序任意有限长度的输入序列。

6. 时空间复杂度

6.1 时间复杂度

情况时间复杂度原因
最好情况Θ(nlogn)\Theta(n\log n)即使输入已经有序,标准实现仍会完成全部划分与合并
平均情况Θ(nlogn)\Theta(n\log n)递归树约有 logn\log n 层,每层处理 Θ(n)\Theta(n) 个元素
最坏情况Θ(nlogn)\Theta(n\log n)划分始终平衡,合并始终为线性时间

递归树中:

  • 树高为 Θ(logn)\Theta(\log n)
  • 每一层合并的元素总数为 nn,工作量为 Θ(n)\Theta(n)

因此:

T(n)=Θ(n)+Θ(n)++Θ(n)Θ(logn) 层=Θ(nlogn)T(n) = \underbrace{\Theta(n)+\Theta(n)+\cdots+\Theta(n)} _{\Theta(\log n)\text{ 层}} = \Theta(n\log n)

渐近最优性

在一般比较模型下,任意比较排序的最坏时间复杂度下界为 Ω(nlogn)\Omega(n\log n)
因此,归并排序在渐近意义上是最优的比较排序算法之一。

6.2 空间复杂度

标准数组实现需要临时数组保存合并结果:

空间来源复杂度
合并辅助数组Θ(n)\Theta(n)
递归调用栈Θ(logn)\Theta(\log n)
总辅助空间Θ(n)\Theta(n)

由于 Θ(n)\Theta(n) 支配 Θ(logn)\Theta(\log n),总辅助空间复杂度为:

Θ(n)\Theta(n)

链表版本

对链表执行归并排序时,可以通过修改节点指针完成合并,不需要长度为 nn 的辅助数组。此时额外节点空间可降为 Θ(1)\Theta(1),但递归实现仍需要 Θ(logn)\Theta(\log n) 的调用栈。

7. 算法性质

性质标准数组归并排序
排序方式比较排序
设计范式分治
稳定性稳定,前提是相等时优先取左侧元素
原地性通常不是原地排序
是否自适应标准实现不是
最坏时间保证Θ(nlogn)\Theta(n\log n)
是否适合链表适合
是否适合外部排序适合
是否易于并行化较适合

8. 需要问题具有的性质

归并排序不仅能用于数字数组。只要问题满足以下条件,就可以使用归并排序思想。

8.1 元素之间可以进行一致的比较

必须存在比较规则,用于判断任意两个元素的先后关系。

比较器通常应满足:

  1. 自反性

    xxx\leq x
  2. 反对称性

    xyx\leq yyxy\leq x,则二者在排序关键字上等价。

  3. 传递性

    xyyzxzx\leq y \land y\leq z \Rightarrow x\leq z
  4. 完全性

    对任意 x,yx,y,至少可以确定 xyx\leq yyxy\leq x

若比较器不满足传递性,排序结果可能不具有一致意义。

8.2 问题可以分解为同类子问题

原问题应能够划分为两个或多个规模更小、结构相同的子问题。

对于排序问题:

  • 原问题:排序一个序列;
  • 子问题:排序该序列的两个子序列。

8.3 子问题的解可以有效合并

仅能划分问题还不够。必须存在高效的合并过程,将子问题的解组合为原问题的解。

归并排序高效的核心在于:

两个已经有序的序列可以在线性时间内合并。

8.4 能够接受相应的存储或访问方式

标准数组归并排序需要 Θ(n)\Theta(n) 辅助空间。若内存限制严格,应考虑:

  • 堆排序
  • 原地归并算法;
  • 链表归并排序;
  • 分块或外部归并排序。

9. 典型例子

对数组执行升序排序:

A = [8, 2, 4, 9, 3, 6]

9.1 分解阶段

[8, 2, 4, 9, 3, 6]
├── [8, 2, 4]
│   ├── [8]
│   └── [2, 4]
│       ├── [2]
│       └── [4]
└── [9, 3, 6]
    ├── [9]
    └── [3, 6]
        ├── [3]
        └── [6]

9.2 合并阶段

[2] + [4]       -> [2, 4]
[8] + [2, 4]    -> [2, 4, 8]

[3] + [6]       -> [3, 6]
[9] + [3, 6]    -> [3, 6, 9]

[2, 4, 8] + [3, 6, 9]
                 -> [2, 3, 4, 6, 8, 9]

最终结果:

[2, 3, 4, 6, 8, 9]

9.3 最后一次合并的指针过程

步骤左侧候选右侧候选取出元素临时结果
1232[2]
2433[2, 3]
3464[2, 3, 4]
4866[2, 3, 4, 6]
5898[2, 3, 4, 6, 8]
699[2, 3, 4, 6, 8, 9]

10. Python 实现

def merge_sort(arr: list[int]) -> None:
    """使用稳定归并排序更新 arr,内部使用 O(n) 辅助数组。"""
    temp = arr.copy()

    def merge(left: int, mid: int, right: int) -> None:
        i = left
        j = mid
        k = left

        while i < mid and j < right:
            # 相等时优先选择左侧元素,从而保持稳定性。
            if arr[i] <= arr[j]:
                temp[k] = arr[i]
                i += 1
            else:
                temp[k] = arr[j]
                j += 1
            k += 1

        while i < mid:
            temp[k] = arr[i]
            i += 1
            k += 1

        while j < right:
            temp[k] = arr[j]
            j += 1
            k += 1

        for index in range(left, right):
            arr[index] = temp[index]

    def sort(left: int, right: int) -> None:
        if right - left <= 1:
            return

        mid = left + (right - left) // 2
        sort(left, mid)
        sort(mid, right)
        merge(left, mid, right)

    sort(0, len(arr))


if __name__ == "__main__":
    data = [8, 2, 4, 9, 3, 6]
    merge_sort(data)
    print(data)

    assert data == [2, 3, 4, 6, 8, 9]

预期输出:

[2, 3, 4, 6, 8, 9]

11. 常见错误

边界定义混用

不要在同一实现中混用闭区间 [left, right] 与左闭右开区间 [left, right)
本文始终使用 [left, right),基础情况为:

right - left <= 1

忘记复制剩余元素

主比较循环结束只说明某一侧已经耗尽,另一侧可能仍有未处理元素,必须继续追加。

破坏稳定性

当关键字相等时先取右侧元素,会改变相等元素原有的相对次序。

在每一层频繁创建切片

某些语言中的数组切片会复制数据。若递归时不断创建左右切片,可能增加常数开销和内存分配。使用索引区间和复用辅助数组通常更高效。

12. 适用场景

归并排序常用于:

  • 需要稳定排序的记录;
  • 对链表进行排序;
  • 数据量过大、无法一次全部载入内存的外部排序;
  • 合并多个已经有序的数据流或文件;
  • 并行排序;
  • 统计数组中的逆序对;
  • 作为 TimSort 等混合排序算法的组成部分。

13. 与其他排序算法比较

算法最好时间平均时间最坏时间辅助空间稳定原地
归并排序Θ(nlogn)\Theta(n\log n)Θ(nlogn)\Theta(n\log n)Θ(nlogn)\Theta(n\log n)Θ(n)\Theta(n)通常否
插入排序Θ(n)\Theta(n)Θ(n2)\Theta(n^2)Θ(n2)\Theta(n^2)Θ(1)\Theta(1)
快速排序Θ(nlogn)\Theta(n\log n)Θ(nlogn)\Theta(n\log n)Θ(n2)\Theta(n^2)平均 Θ(logn)\Theta(\log n)通常否通常是
堆排序Θ(nlogn)\Theta(n\log n)Θ(nlogn)\Theta(n\log n)Θ(nlogn)\Theta(n\log n)Θ(1)\Theta(1)

归并排序与快速排序的核心区别

  • 归并排序:分解简单,合并复杂
  • 快速排序:分解复杂,合并简单

归并排序提供稳定的最坏时间保证,但通常需要额外数组;快速排序通常具有更好的缓存局部性和更小的常数开销,但最坏情况可能退化为 Θ(n2)\Theta(n^2)

14. 总结

归并排序的核心结构是:

划分 -> 递归排序左半部分 -> 递归排序右半部分 -> 线性合并

其关键结论为:

T(n)=2T(n2)+Θ(n)=Θ(nlogn)T(n) = 2T\left(\frac{n}{2}\right) + \Theta(n) = \Theta(n\log n)

需要特别记住:

  1. MERGE 的输入必须是两个有序序列;
  2. 标准数组实现需要 Θ(n)\Theta(n) 辅助空间;
  3. 相等时优先取左侧元素可以保证稳定性;
  4. 归并排序的最好、平均和最坏时间复杂度均为 Θ(nlogn)\Theta(n\log n)
  5. 它非常适合链表、外部排序和需要最坏时间保证的场景。

15. 参考资料

  1. Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, Clifford Stein. Introduction to Algorithms, 3rd Edition. MIT Press, 2009. Section 2.3.
  2. Erik D. Demaine, Charles E. Leiserson. Introduction to Algorithms — Lecture 1: Analysis of Algorithms. MIT.
  3. Erik D. Demaine, Charles E. Leiserson. Introduction to Algorithms — Lecture 3: Divide and Conquer. MIT.

16. 相关笔记