Part 3.3:归并排序:稳定高效的分治典范

引言:稳定性的代价与价值

在上一篇文章中,我们学习了快速排序——平均性能最优但不稳定的排序算法。今天,我们将学习它的”孪生兄弟”——归并排序 (Merge Sort),一个保证 O(n log n) 时间复杂度且稳定的排序算法。

归并排序由约翰·冯·诺依曼在 1945 年发明,是分治思想的又一经典体现。虽然它需要额外的 O(n) 空间,但其稳定性和可预测的性能使其在许多场景中不可替代。

为什么归并排序重要?

  • 稳定性保证:当需要保持相等元素的相对顺序时(如多关键字排序),归并排序是首选。
  • 外部排序:处理无法一次性加载到内存的大数据时,归并排序是最佳选择。
  • 并行化:归并排序天然适合并行计算,在多核处理器上性能优异。

本文将带你深入理解归并排序的原理、实现和应用。

1. 核心原理:分解与合并

归并排序采用分治策略,其思想可以概括为两个步骤:

  1. **分解 (Divide)**:将数组递归地分成两半,直到每个子数组只有一个元素(单个元素天然有序)。
  2. **合并 (Merge)**:将两个已排序的子数组合并成一个有序数组。

关键图示:归并排序的递归树

图示

初始数组: [38, 27, 43, 3, 9, 82, 10]

分解过程:
        [38, 27, 43, 3, 9, 82, 10]
       /                          \
  [38, 27, 43, 3]              [9, 82, 10]
   /           \                /         \
[38, 27]     [43, 3]         [9, 82]     [10]
 /    \       /    \          /    \
[38]  [27]  [43]  [3]       [9]  [82]

合并过程:
[27, 38]     [3, 43]         [9, 82]     [10]
   \           /                \         /
  [3, 27, 38, 43]              [9, 10, 82]
       \                          /
        [3, 9, 10, 27, 38, 43, 82]

2. 核心操作:合并 (Merge)

归并排序的核心是合并两个已排序数组的过程。

算法思路

假设我们要合并两个已排序的子数组 leftright

  1. 创建一个临时数组 temp
  2. 使用两个指针 ij 分别指向 leftright 的开头。
  3. 比较 left[i]right[j],将较小的元素放入 temp,并移动对应的指针。
  4. 重复步骤 3,直到某一个数组的元素全部放入 temp
  5. 将另一个数组的剩余元素全部放入 temp
  6. temp 复制回原数组。

动画式案例

合并 [27, 38][3, 43]

  1. i=0, j=0:比较 27 和 3,3 更小,放入 temp → [3]j++
  2. i=0, j=1:比较 27 和 43,27 更小,放入 temp → [3, 27]i++
  3. i=1, j=1:比较 38 和 43,38 更小,放入 temp → [3, 27, 38]i++
  4. left 已用完,将 right 剩余元素放入 → [3, 27, 38, 43]

伪代码

function mergeSort(arr, left, right):
  if left < right:
    mid = (left + right) // 2
    mergeSort(arr, left, mid)
    mergeSort(arr, mid + 1, right)
    merge(arr, left, mid, right)

function merge(arr, left, mid, right):
  // 创建临时数组
  L = arr[left...mid]
  R = arr[mid+1...right]
  
  i = 0, j = 0, k = left
  
  // 合并
  while i < len(L) and j < len(R):
    if L[i] <= R[j]:
      arr[k] = L[i]
      i++
    else:
      arr[k] = R[j]
      j++
    k++
  
  // 复制剩余元素
  while i < len(L):
    arr[k] = L[i]
    i++, k++
  
  while j < len(R):
    arr[k] = R[j]
    j++, k++

Python 实现

def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    
    mid = len(arr) // 2
    left = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    
    return merge(left, right)

def merge(left, right):
    result = []
    i = j = 0
    
    while i < len(left) and j < len(right):
        if left[i] <= right[j]:
            result.append(left[i])
            i += 1
        else:
            result.append(right[j])
            j += 1
    
    result.extend(left[i:])
    result.extend(right[j:])
    
    return result

# 使用
arr = [38, 27, 43, 3, 9, 82, 10]
sorted_arr = merge_sort(arr)
print(sorted_arr) # 输出: [3, 9, 10, 27, 38, 43, 82]

3. 复杂度分析

时间复杂度

  • 所有情况O(n log n)。无论数据如何分布,归并排序的时间复杂度都是稳定的。
    • 分解:log n 层(二分)
    • 合并:每层需要 O(n) 时间
    • 总时间:O(n log n)

空间复杂度

  • O(n):需要额外的临时数组来存储合并结果。

稳定性

稳定。在合并时,如果 left[i] == right[j],我们总是先取 left[i],从而保持了相等元素的相对顺序。

4. 归并排序的应用

4.1 外部排序

当数据量巨大,无法一次性加载到内存时(如排序 100GB 的文件),可以使用归并排序的思想:

  1. 将大文件分成多个小文件,分别排序。
  2. 使用多路归并,将这些小文件合并成一个大的有序文件。

4.2 计算逆序对

问题:给定一个数组,计算其中有多少对 (i, j) 满足 i < jarr[i] > arr[j]

解法:在归并排序的合并过程中,当从右子数组取元素时,说明左子数组中剩余的所有元素都与该元素构成逆序对。

时间复杂度O(n log n),远优于暴力的 O(n²)

4.3 链表排序

归并排序非常适合链表排序,因为链表的合并操作不需要额外空间,只需修改指针。

5. 归并排序 vs 快速排序

特性归并排序快速排序
时间复杂度(最坏)O(n log n)O(n²)
空间复杂度O(n)O(log n)
稳定性稳定不稳定
原地排序
实际性能稳定但需额外空间通常更快
适用场景需要稳定性、外部排序、链表排序一般场景、内存排序

6. 总结

归并排序通过”分解-合并”的分治策略,实现了稳定的 O(n log n) 排序。

  • 核心:合并两个已排序数组。
  • 优势:时间复杂度稳定、稳定排序、适合外部排序。
  • 劣势:需要 O(n) 额外空间。
  • 应用:外部排序、逆序对计数、链表排序。

掌握了归并排序,你就理解了分治思想的另一种经典应用。在下一篇文章中,我们将学习基于堆的排序算法——堆排序


Part 3.3:归并排序:稳定高效的分治典范
https://hjjjkh.github.io/posts/78eaaad9
作者
李国强
发布于
2025年11月15日
许可协议