首页/数据结构/07-sorting/归并排序 🔗 在 Obsidian 中打开
数据结构 · 07-sorting

归并排序

难度 ★★★重要度 ★★ 考查频率 低题型 分治法 数据结构/排序分治法稳定
速查
归并排序是基于分治法的排序:先递归分解,再合并两个有序子序列。时间复杂度始终 $O(n\log_2 n)$,空间 $O(n)$(辅助数组),稳定,与初始序列无关;适合链表与外部排序。

速查

项目
主题归并排序
核心概念基于分治法,将有序子序列合并为整体有序
时间复杂度$O(n\log_2 n)$(最好/平均/最坏均同阶)
空间复杂度$O(n)$(辅助数组)
稳定性✅ 稳定
难度⭐⭐⭐

核心概念

归并排序(Merge Sort)是基于分治法的排序算法,由 John von Neumann 在 1945 年提出。"归并"是指将两个或多个已经有序的序列合并为一个有序序列的过程。

核心思想

  1. 分解(Divide):将长度为 n 的序列分成两个长度为 n/2 的子序列
  2. 解决(Conquer):递归地对两个子序列进行归并排序
  3. 合并(Merge):将两个已排序的子序列合并为一个有序序列

归并排序的递归树是一棵倒立的完全二叉树,每层归并的时间复杂度为 $O(n)$,共 $\log_2 n$ 层,总时间复杂度为 $O(n\log_2 n)$。

关键特性

  • 归并排序是稳定的排序算法
  • 时间复杂度始终为 $O(n\log_2 n)$,与初始序列无关(最好、最坏、平均都一样)
  • 需要额外的 $O(n)$ 空间用于合并操作,是内部排序中空间消耗最大的
  • 适合对链表进行排序(不需要额外空间)
  • 外部排序的基础算法

算法步骤

二路归并排序过程

  1. 分解:将长度为 n 的序列从中间分成两半,得到两个子序列
  2. 递归排序:对左子序列和右子序列分别递归执行归并排序
  3. 合并:将两个已排序的子序列合并为一个有序序列

合并(Merge)过程

设两个有序子序列为 L[i..m]L[m+1..r],合并步骤:

  1. 设置三个指针:i 指向第一个子序列开头,j 指向第二个子序列开头,k 指向辅助数组开头
  2. 比较 L[i]L[j],将较小的元素放入辅助数组 temp[k],相应指针后移
  3. 重复步骤 2,直到某个子序列的所有元素都已放入辅助数组
  4. 将另一个子序列的剩余元素全部复制到辅助数组末尾
  5. 将辅助数组中的有序结果复制回原数组

归并趟数

n 个元素进行二路归并排序,归并趟数为 $\lceil\log_2 n\rceil$

代码实现

// 合并两个有序子序列 A[low..mid] 和 A[mid+1..high]
void Merge(int A[], int low, int mid, int high) {
    // 将 A 中元素复制到辅助数组 temp
    for (int k = low; k <= high; k++)
        temp[k] = A[k];

    int i = low, j = mid + 1, k = low;
    // 比较两个子序列的元素,按顺序放入原数组
    while (i <= mid && j <= high) {
        if (temp[i] <= temp[j])  // 注意:<= 保证稳定性
            A[k++] = temp[i++];
        else
            A[k++] = temp[j++];
    }
    // 处理剩余元素
    while (i <= mid) A[k++] = temp[i++];
    while (j <= high) A[k++] = temp[j++];
}

// 归并排序(递归实现)
void MergeSort(int A[], int low, int high) {
    if (low < high) {
        int mid = low + (high - low) / 2;  // 从中间划分
        MergeSort(A, low, mid);            // 对左半部分归并排序
        MergeSort(A, mid + 1, high);       // 对右半部分归并排序
        Merge(A, low, mid, high);          // 合并两个有序子序列
    }
}

// 初始化调用
void Sort(int A[], int n) {
    temp = (int *)malloc(n * sizeof(int));
    MergeSort(A, 0, n - 1);
    free(temp);
}

手算示例

对序列 {49, 38, 65, 97, 76, 13, 27} 进行归并排序。先递归分解为单元素,再自底向上合并;下面以最后一步合并为例:

左半已排好 [38, 49, 65, 97],右半已排好 [13, 27, 76],合并过程:

  1. 比较 38 与 13 → 取 13,结果 [13]
  2. 比较 38 与 27 → 取 27,结果 [13, 27]
  3. 比较 38 与 76 → 取 38,结果 [13, 27, 38]
  4. 比较 49 与 76 → 取 49,结果 [13, 27, 38, 49]
  5. 比较 65 与 76 → 取 65,结果 [13, 27, 38, 49, 65]
  6. 比较 97 与 76 → 取 76,结果 [13, 27, 38, 49, 65, 76]
  7. 右半已空,复制剩余 97,结果 [13, 27, 38, 49, 65, 76, 97]

这正是右侧交互动画演示的内容。

复杂度分析

时间复杂度

情况时间复杂度说明
最好情况$O(n\log_2 n)$序列有序时
平均情况$O(n\log_2 n)$随机输入
最坏情况$O(n\log_2 n)$序列逆序时
  • 归并排序的递归树是一棵完全二叉树,高度为 $\lceil\log_2 n\rceil$
  • 每一层归并操作需要处理所有 n 个元素,时间复杂度为 $O(n)$
  • 总时间复杂度 $= O(n) \times \lceil\log_2 n\rceil =$ $O(n\log_2 n)$
  • 无论初始序列如何,时间复杂度始终为 $O(n\log_2 n)$

空间复杂度

空间复杂度为 $O(n)$

  • 需要一个长度为 n 的辅助数组用于合并操作
  • 递归调用栈的深度为 $O(\log_2 n)$
  • 总空间复杂度 = $O(n) + O(\log_2 n)$ = $O(n)$
  • 归并排序是内部排序中空间消耗最大的算法

稳定性分析

归并排序是稳定的排序算法。

原因:在合并过程中,当两个子序列中的元素相等时(temp[i] == temp[j]),我们总是先取第一个子序列的元素(使用 <= 比较),这保证了相等元素的相对顺序不变。

常见考法

  1. 手动模拟归并过程:给出两个有序子序列,要求写出合并结果
  2. 归并趟数计算:n 个元素的归并趟数为 $\lceil\log_2 n\rceil$
  3. 归并排序与其他排序的比较:时间复杂度稳定,但空间复杂度高
  4. 归并排序的适用场景:适合大规模数据、链表排序、外部排序
  5. 归并排序的稳定性:归并排序是稳定的,快速排序和堆排序不稳定
  6. 归并排序的空间复杂度:$O(n)$,是内部排序中最大的
  7. k 路归并:m 路归并排序的趟数为 $\lceil\log_k N\rceil$

易错点

  1. ❌ 认为归并排序的空间复杂度是 $O(1)$ → $O(n)$,需要辅助数组
  2. ❌ 合并时忘记处理剩余元素 → 两个 while 循环只有一个会执行
  3. ❌ 混淆归并趟数和递归深度 → 归并趟数是 $\lceil\log_2 n\rceil$
  4. ❌ 认为归并排序不适合链表 → 非常适合,链表不需要额外空间
  5. ❌ 合并时使用 < 而不是 <= → 会导致不稳定
  6. ❌ 认为归并排序的时间复杂度与初始序列有关 → 始终 $O(n\log_2 n)$

核心结论

  1. 归并排序时间复杂度始终为 $O(n\log_2 n)$,与初始序列无关(最好/平均/最坏相同)
  2. 空间复杂度为 $O(n)$,是内部排序中空间消耗最大的
  3. 归并排序是稳定
  4. 归并趟数为 $\lceil\log_2 n\rceil$
  5. 适合链表排序外部排序
  6. 归并排序的递归树是完全二叉树
  7. 合并时使用 <= 比较保证稳定性
  8. 较大数据排序时用归并排序,用空间换时间

记忆卡片

归并排序的时间复杂度在最好、平均、最坏情况下分别是多少?
全部都是 $O(n\log_2 n)$。归并排序的时间复杂度与初始序列无关。
归并排序的空间复杂度为什么是 $O(n)$?
因为合并两个有序子序列时需要一个长度为 n 的辅助数组。递归栈深度为 $O(\log_2 n)$,总空间为 $O(n)$。
归并排序为什么是稳定的?
合并时,当两个子序列的元素相等,总是先取第一个子序列的元素(使用 <= 比较),保证了相等元素的相对顺序不变。
n 个元素进行二路归并排序,需要多少趟?
$\lceil\log_2 n\rceil$ 趟。每趟将相邻的有序子序列两两合并。
归并排序为什么适合外部排序?
因为归并排序只需顺序访问数据,不需要随机访问。在外部排序中,可将数据分块读入内存排序后再归并,减少磁盘 I/O。

交互动画

左半 右半 结果 38 49 65 97 13 27 76 i j k
第 0 / 7 步
初始:左半 [38,49,65,97],右半 [13,27,76];准备归并
怎么看指针 i、j 分别扫左/右半,k 指向结果下一格。每步比较 L[i] 与 R[j],把较小的放进结果(浅橙=正在比较,深橙=已落位)。

相关知识点

(暂无关联知识点)