归并排序(Merge Sort)是计算机科学中最早被提出的排序算法之一,由约翰-冯-诺伊曼(John von Neumann)于1945年在其著名的EDVAC计算机报告中首次描述。归并排序是分治算法(Divide and Conquer)最典型的代表之一,它的核心思想极其简洁却威力强大:将一个大问题分解为若干小问题,分别解决后将结果合并。
在排序领域中,归并排序拥有几个非常引人注目的特性:
| 指标 | 数值 | 说明 |
|---|---|---|
| 最优时间复杂度 | O(n log n) | 无论数据分布如何,均稳定在此复杂度 |
| 平均时间复杂度 | O(n log n) | 概率意义上的期望时间复杂度 |
| 最坏时间复杂度 | O(n log n) | 不存在快速排序的退化问题 |
| 空间复杂度 | O(n) | 需要与原数组等大的辅助数组 |
| 稳定性 | 稳定 | 相等元素的相对顺序保持 |
| 是否原地排序 | 否 | 需要额外 O(n) 空间 |
归并排序的核心思想可以用"分而治之"四个字来概括。它将排序一个大数组的问题拆解为两个核心操作:
合并两个有序数组的操作是归并排序的核心。假设我们有两个已经排好序的数组 A 和 B,合并的步骤如下:
合并过程的时间复杂度为 O(n),其中 n 是两个子数组的长度之和。合并过程只需要一次遍历,且每个元素只被比较和移动一次。
"合久必分,分久必合。"——归并排序的精髓就在于先无脑地拆分到最简,再精心地合并成有序。
从更高的视角来看,归并排序的过程可以用一棵递归树来描述。树的每一层对应一次分解或合并操作。在分解阶段,数组被逐层二分,直到每个叶子节点只包含一个元素;在合并阶段,叶子节点逐层合并,直到根节点得到完整的有序数组。
自顶向下(Top-Down)的归并排序是最直观的实现方式。它直接对应分治算法的三个步骤:递归地将数组分成两半,分别排序,然后合并。这是最符合"分治法"思维模式的实现版本,也是教学中最常用的展示方式。
def merge_sort(arr):
"""归并排序(自顶向下递归实现)"""
if len(arr) <= 1:
return arr
# 1. 分解:找到中间位置,一分为二
mid = len(arr) // 2
left = arr[:mid]
right = arr[mid:]
# 2. 解决:递归地对左右两半进行排序
left = merge_sort(left)
right = merge_sort(right)
# 3. 合并:将两个有序数组合并为一个
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
上面的实现每次递归都创建新的列表,会产生大量的内存分配和复制开销。更高效的做法是使用一个全局辅助数组,在原数组上进行原地合并操作。
def merge_sort_inplace(arr):
"""归并排序(原地修改版)"""
n = len(arr)
# 预分配辅助数组,避免反复创建
temp = [0] * n
_merge_sort(arr, 0, n - 1, temp)
return arr
def _merge_sort(arr, left, right, temp):
"""递归函数:对 arr[left..right] 进行排序"""
if left >= right:
return
# 分解
mid = left + (right - left) // 2
# 解决
_merge_sort(arr, left, mid, temp)
_merge_sort(arr, mid + 1, right, temp)
# 合并
_merge(arr, left, mid, right, temp)
def _merge(arr, left, mid, right, temp):
"""合并 arr[left..mid] 和 arr[mid+1..right]"""
# 复制到辅助数组
for i in range(left, right + 1):
temp[i] = arr[i]
i = left # 左半部分起始索引
j = mid + 1 # 右半部分起始索引
k = left # 原数组合并位置索引
while i <= mid and j <= right:
if temp[i] <= temp[j]:
arr[k] = temp[i]
i += 1
else:
arr[k] = temp[j]
j += 1
k += 1
# 处理左半部分剩余元素(右半部分剩余元素已在原位)
while i <= mid:
arr[k] = temp[i]
i += 1
k += 1
以数组 [38, 27, 43, 3, 9, 82, 10] 为例,自顶向下归并排序的执行过程如下:
初始数组: [38, 27, 43, 3, 9, 82, 10]
=== 分解阶段 ===
第1层分解: [38, 27, 43, 3] | [9, 82, 10]
第2层分解: [38, 27] | [43, 3] | [9, 82] | [10]
第3层分解: [38] | [27] | [43] | [3] | [9] | [82] | [10]
=== 合并阶段 ===
第3层合并: [27, 38] | [3, 43] | [9, 82] | [10]
第2层合并: [3, 27, 38, 43] | [9, 10, 82]
第1层合并: [3, 9, 10, 27, 38, 43, 82]
最终有序数组: [3, 9, 10, 27, 38, 43, 82]
自底向上(Bottom-Up)的归并排序是另一种实现策略。与自顶向下不同,它不使用递归,而是直接将数组视为若干个长度为 1 的有序子数组,然后通过迭代逐步增大归并的长度,直到整个数组有序。
自底向上的实现方式避免了递归调用的开销,同时消除了递归深度带来的栈空间消耗,在某些场景下性能更优。此外,对于不支持递归或递归开销较大的环境(如某些嵌入式系统),自底向上是唯一可行的实现方式。
def merge_sort_bottom_up(arr):
"""归并排序(自底向上迭代实现)"""
n = len(arr)
temp = [0] * n
size = 1 # 子数组大小,从 1 开始倍增长
while size < n:
# 遍历数组,对相邻的两个子数组进行合并
for left in range(0, n - size, size * 2):
mid = left + size - 1
right = min(left + size * 2 - 1, n - 1)
_merge(arr, left, mid, right, temp)
size *= 2 # 子数组大小倍增
return arr
def _merge(arr, left, mid, right, temp):
"""合并 arr[left..mid] 和 arr[mid+1..right]"""
for i in range(left, right + 1):
temp[i] = arr[i]
i = left
j = mid + 1
k = left
while i <= mid and j <= right:
if temp[i] <= temp[j]:
arr[k] = temp[i]
i += 1
else:
arr[k] = temp[j]
j += 1
k += 1
while i <= mid:
arr[k] = temp[i]
i += 1
k += 1
对于数组 [38, 27, 43, 3, 9, 82, 10],自底向上归并排序的执行过程:
初始数组: [38, 27, 43, 3, 9, 82, 10]
size=1: 合并相邻的长度为1的子数组
合并 [38]和[27] => [27, 38]
合并 [43]和[3] => [3, 43]
合并 [9]和[82] => [9, 82]
合并 [10]单独 => [10]
结果: [27, 38, 3, 43, 9, 82, 10]
size=2: 合并相邻的长度为2的子数组
合并 [27,38]和[3,43] => [3, 27, 38, 43]
合并 [9,82]和[10] => [9, 10, 82]
结果: [3, 27, 38, 43, 9, 10, 82]
size=4: 合并相邻的长度为4的子数组
合并 [3,27,38,43]和[9,10,82] => [3, 9, 10, 27, 38, 43, 82]
结果: [3, 9, 10, 27, 38, 43, 82]
| 对比维度 | 自顶向下(递归) | 自底向上(迭代) |
|---|---|---|
| 编程复杂度 | 低,代码简洁直观 | 中等,边界处理稍复杂 |
| 递归/栈开销 | 有递归调用和栈空间开销 | 无递归,完全避免栈问题 |
| 空间复杂度 | O(n + log n)(辅助数组 + 递归栈) | O(n)(仅辅助数组) |
| 缓存友好性 | 较差(跳跃访问模式) | 较好(顺序归并) |
| 链表排序 | 较易实现 | 较易实现(不需要额外空间) |
| 并行化 | 天然适合并行 | 需要额外设计 |
在实际工程中,自顶向下实现更常用于教学和通用场景(代码简洁性优先),而自底向上实现则在性能敏感型场景和对递归深度有限制的环境中更受青睐。对于大规模数据的排序,自底向上的迭代实现往往具有更好的常数因子性能。
虽然基础的归并排序已经具有 O(n log n) 的稳定性能,但通过一系列优化手段,我们可以进一步提升其实际运行效率。以下是最常用的几种优化策略。
当子数组规模较小时(通常阈值设为 7-15 个元素),插入排序的实际运行速度反而快于归并排序。这是因为插入排序在小规模数据上没有递归开销,且利用了 CPU 缓存的局部性原理。这一优化技巧被称为混合排序(Hybrid Sort),也是 TimSort(Python 和 Java 标准库使用的排序算法)的核心思想之一。
INSERTION_THRESHOLD = 10 # 小数组阈值
def merge_sort_optimized(arr):
"""优化后的归并排序——小数组使用插入排序"""
n = len(arr)
temp = [0] * n
_merge_sort_opt(arr, 0, n - 1, temp)
return arr
def _merge_sort_opt(arr, left, right, temp):
"""递归函数:小数组使用插入排序"""
# 小数组使用插入排序
if right - left + 1 <= INSERTION_THRESHOLD:
insertion_sort(arr, left, right)
return
mid = left + (right - left) // 2
_merge_sort_opt(arr, left, mid, temp)
_merge_sort_opt(arr, mid + 1, right, temp)
# 如果数组已经有序,跳过合并
if arr[mid] <= arr[mid + 1]:
return
_merge(arr, left, mid, right, temp)
def insertion_sort(arr, left, right):
"""对 arr[left..right] 进行插入排序"""
for i in range(left + 1, right + 1):
key = arr[i]
j = i - 1
while j >= left and arr[j] > key:
arr[j + 1] = arr[j]
j -= 1
arr[j + 1] = key
基础实现中,每次 merge 调用都创建新的列表(或数组),这会导致大量的内存分配和垃圾回收开销。优化的方式是在递归开始前一次性预分配一个与原数组等大的辅助数组,并在整个排序过程中复用同一个数组。
def merge_sort_no_alloc(arr):
"""预分配辅助数组版本,避免重复分配"""
n = len(arr)
temp = [0] * n
_merge_sort_no_alloc(arr, 0, n - 1, temp)
return arr
def _merge_sort_no_alloc(arr, left, right, temp):
if left >= right:
return
mid = left + (right - left) // 2
_merge_sort_no_alloc(arr, left, mid, temp)
_merge_sort_no_alloc(arr, mid + 1, right, temp)
# 只有当需要合并时才复制
if arr[mid] > arr[mid + 1]:
for i in range(left, right + 1):
temp[i] = arr[i]
i, j, k = left, mid + 1, left
while i <= mid and j <= right:
if temp[i] <= temp[j]:
arr[k] = temp[i]
i += 1
else:
arr[k] = temp[j]
j += 1
k += 1
while i <= mid:
arr[k] = temp[i]
i += 1
k += 1
在合并前检查 arr[mid] <= arr[mid+1]:如果左右两半的最大值已经小于等于右半的最小值,说明两个子数组已经整体有序,可以跳过合并步骤。这在处理近乎有序的数据时能带来显著的性能提升。
排序算法的稳定性(Stability)是指:如果待排序序列中存在值相等的元素,经过排序后,这些相等元素的相对顺序保持不变。
归并排序是稳定排序。这一性质源自合并操作中的关键决策:当两个子数组中存在相等元素时,我们优先取左侧子数组的元素。具体来说,在 _merge 函数的比较语句中,使用 temp[i] <= temp[j](而不是 <)保证了当左右两边的元素相等时,来自左侧子数组(原来就在前面)的元素被先放入结果中。
# 稳定版本:使用 <=,相等时优先取左侧元素
if temp[i] <= temp[j]:
arr[k] = temp[i] # 左侧元素保持在前
i += 1
# 不稳定版本:使用 <,相等时取右侧元素(破坏了稳定性)
if temp[i] < temp[j]:
arr[k] = temp[i]
i += 1
在对多个字段进行排序时,稳定性非常重要。例如,先按"成绩"排序,再按"班级"排序——如果排序算法是稳定的,那么第二次排序后,同一班级内的学生将依然保持按成绩排序的顺序。归并排序的稳定性使其特别适合此类多关键字排序场景。
归并排序的独特性质使其在以下场景中得到广泛应用。
当待排序数据量远超计算机内存容量时,传统的内部排序算法(如快速排序)无法工作,因为数据无法一次性载入内存。此时,归并排序的思想被用于外部排序——这是归并排序最重要的工程应用之一。
import heapq
def k_way_merge(sorted_arrays):
"""
多路归并:将 K 个有序数组合并为一个有序数组
使用最小堆(优先队列)来提高效率
"""
result = []
# 最小堆,存储 (值, 数组索引, 元素索引)
heap = []
# 初始化堆:将每个数组的第一个元素入堆
for i, arr in enumerate(sorted_arrays):
if arr:
heapq.heappush(heap, (arr[0], i, 0))
# 不断取堆顶最小元素,并从相应数组中补充下一个元素
while heap:
val, arr_idx, elem_idx = heapq.heappop(heap)
result.append(val)
# 如果该数组还有下一个元素,入堆
if elem_idx + 1 < len(sorted_arrays[arr_idx]):
next_val = sorted_arrays[arr_idx][elem_idx + 1]
heapq.heappush(heap, (next_val, arr_idx, elem_idx + 1))
return result
# 示例:5个有序数组的多路归并
arrays = [
[3, 7, 15],
[1, 8, 20],
[5, 12, 18],
[2, 10, 22],
[6, 9, 11]
]
result = k_way_merge(arrays)
print(result) # [1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 15, 18, 20, 22]
由于链表的随机访问效率极低(O(n) 时间才能找到中间节点),快速排序和堆排序在链表上表现不佳。而归并排序只需要顺序访问数据,天然适合链表的物理结构。使用归并排序对链表排序,甚至可以实现 O(1) 的额外空间复杂度。
class ListNode:
"""单向链表节点"""
def __init__(self, val=0, next=None):
self.val = val
self.next = next
def merge_sort_linked_list(head):
"""对单向链表进行归并排序,空间复杂度 O(1)"""
if not head or not head.next:
return head
# 1. 快慢指针找到链表的中点
slow = head
fast = head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 2. 分割链表
right_head = slow.next
slow.next = None
# 3. 递归排序左右两部分
left = merge_sort_linked_list(head)
right = merge_sort_linked_list(right_head)
# 4. 合并两个有序链表
return merge_two_lists(left, right)
def merge_two_lists(l1, l2):
"""合并两个有序链表"""
dummy = ListNode(0)
curr = dummy
while l1 and l2:
if l1.val <= l2.val:
curr.next = l1
l1 = l1.next
else:
curr.next = l2
l2 = l2.next
curr = curr.next
curr.next = l1 if l1 else l2
return dummy.next
逆序对是衡量数组有序程度的重要指标。在一个数组中,如果 i < j 但 a[i] > a[j],则 (i, j) 构成一个逆序对。求逆序对的经典解法就是利用归并排序的合并过程——在合并阶段,每当从右半部分取一个元素时,左半部分剩余的所有元素都与该元素构成逆序对。
def count_inversions(arr):
"""利用归并排序计算数组中的逆序对数量"""
n = len(arr)
temp = [0] * n
return _count_inversions(arr, 0, n - 1, temp)
def _count_inversions(arr, left, right, temp):
"""返回 arr[left..right] 中的逆序对数量"""
if left >= right:
return 0
mid = left + (right - left) // 2
count = 0
# 左半部分的逆序对 + 右半部分的逆序对
count += _count_inversions(arr, left, mid, temp)
count += _count_inversions(arr, mid + 1, right, temp)
# 跨左右两半的逆序对
count += _merge_and_count(arr, left, mid, right, temp)
return count
def _merge_and_count(arr, left, mid, right, temp):
"""合并并统计跨两半的逆序对"""
for i in range(left, right + 1):
temp[i] = arr[i]
i, j, k = left, mid + 1, left
count = 0
while i <= mid and j <= right:
if temp[i] <= temp[j]:
arr[k] = temp[i]
i += 1
else:
# 关键:temp[j] < temp[i],且 i < j
# 左半部分从 i 到 mid 的所有元素都大于 temp[j]
count += (mid - i + 1)
arr[k] = temp[j]
j += 1
k += 1
while i <= mid:
arr[k] = temp[i]
i += 1
k += 1
return count
# 示例
arr = [2, 4, 1, 3, 5]
print(count_inversions(arr)) # 输出: 3
# 逆序对:(2,1), (4,1), (4,3)
利用归并排序求逆序对的时间复杂度为 O(n log n),而暴力枚举法的时间复杂度为 O(n2)。对于 n=105 的数据规模,归并排序方法只需不到 0.1 秒,而暴力枚举需要数小时。
值得一提的是,Python 的 list.sort() 和 Java 的 Arrays.sort(Object[]) 所使用的排序算法 TimSort,其本质是归并排序的变体。TimSort 巧妙地结合了归并排序和插入排序,并利用了数据中已有的"自然有序"(Natural Run)序列,在真实数据上的性能极为出色。
| 应用场景 | 使用归并排序的原因 | 典型代表 |
|---|---|---|
| 数据库外部排序 | 顺序访问磁盘,分治处理大文件 | PostgreSQL、MySQL |
| 链表排序 | 不需要随机访问,空间效率高 | 各语言标准库链表排序 |
| 通用高性能排序 | 稳定性 + O(n log n) 保证 | TimSort(Python/Java) |
| 逆序对统计 | 合并过程中的自然计数 | 算法竞赛、数据分析 |
| 并行排序 | 分治结构天然可并行 | 多线程归并排序 |
归并排序的时间复杂度可以用递归关系式来精确描述。设 T(n) 表示对 n 个元素进行归并排序所需的时间,则有:
其中:
上述递推关系式符合主定理(Master Theorem)的标准形式。主定理描述了形如 T(n) = aT(n/b) + f(n) 的递推关系式的渐近复杂度。
对于递推式 T(n) = aT(n/b) + f(n),其中 a >= 1, b > 1,f(n) 是渐近正函数:
代入归并排序的参数:
这符合主定理的情况2,因此:
不使用主定理,我们也可以通过递推展开来验证 O(n log n) 的结果:
T(n) = 2T(n/2) + cn [第1层]
= 2[2T(n/4) + c(n/2)] + cn = 4T(n/4) + 2cn [第2层]
= 4[2T(n/8) + c(n/4)] + 2cn = 8T(n/8) + 3cn [第3层]
= ...
= 2^k T(n/2^k) + kcn [第k层]
当 n/2^k = 1 时,即 k = log₂n 时递归终止:
T(n) = nT(1) + cn log₂n
= O(n) + O(n log n)
= O(n log n)
归并排序的空间复杂度由两部分构成:
因此,总空间复杂度为 O(n + log n) = O(n)。对于自底向上的实现,由于不使用递归,空间复杂度可以降低到 O(n)(仅辅助数组)。
虽然理论上存在原地归并排序(In-Place Merge Sort)算法(如 Kronrod 算法),但其实现极其复杂且常数因子巨大,实际运行速度反而比标准的 O(n) 空间版本慢得多。因此,在工程实践中,O(n) 空间版本的归并排序仍然是唯一的实用选择。这也是"没有免费的午餐"在算法设计中的一个经典体现。
归并排序在不同编程语言中的实现风格各具特色。以下是几个主流语言的实现示例。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void merge(int arr[], int left, int mid, int right, int temp[]) {
int i = left, j = mid + 1, k = left;
// 复制到临时数组
memcpy(temp + left, arr + left, (right - left + 1) * sizeof(int));
while (i <= mid && j <= right) {
if (temp[i] <= temp[j])
arr[k++] = temp[i++];
else
arr[k++] = temp[j++];
}
while (i <= mid)
arr[k++] = temp[i++];
}
void merge_sort(int arr[], int left, int right, int temp[]) {
if (left >= right)
return;
int mid = left + (right - left) / 2;
merge_sort(arr, left, mid, temp);
merge_sort(arr, mid + 1, right, temp);
merge(arr, left, mid, right, temp);
}
void sort(int arr[], int n) {
int *temp = (int *)malloc(n * sizeof(int));
merge_sort(arr, 0, n - 1, temp);
free(temp);
}
public class MergeSort {
public static <T extends Comparable<? super T>> void sort(T[] arr) {
T[] temp = (T[]) new Comparable[arr.length];
sort(arr, 0, arr.length - 1, temp);
}
private static <T extends Comparable<? super T>> void
sort(T[] arr, int left, int right, T[] temp) {
if (left >= right) return;
int mid = left + (right - left) / 2;
sort(arr, left, mid, temp);
sort(arr, mid + 1, right, temp);
// 如果已有序,跳过合并
if (arr[mid].compareTo(arr[mid + 1]) <= 0)
return;
merge(arr, left, mid, right, temp);
}
private static <T extends Comparable<? super T>> void
merge(T[] arr, int left, int mid, int right, T[] temp) {
System.arraycopy(arr, left, temp, left, right - left + 1);
int i = left, j = mid + 1, k = left;
while (i <= mid && j <= right) {
if (temp[i].compareTo(temp[j]) <= 0)
arr[k++] = temp[i++];
else
arr[k++] = temp[j++];
}
while (i <= mid)
arr[k++] = temp[i++];
}
}
package main
func mergeSort(arr []int) []int {
if len(arr) <= 1 {
return arr
}
mid := len(arr) / 2
left := mergeSort(arr[:mid])
right := mergeSort(arr[mid:])
return merge(left, right)
}
func merge(left, right []int) []int {
result := make([]int, 0, len(left)+len(right))
i, j := 0, 0
for i < len(left) && j < len(right) {
if left[i] <= right[j] {
result = append(result, left[i])
i++
} else {
result = append(result, right[j])
j++
}
}
result = append(result, left[i:]...)
result = append(result, right[j:]...)
return result
}
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = mergeSort(arr.slice(0, mid));
const right = mergeSort(arr.slice(mid));
return merge(left, right);
}
function merge(left, right) {
const result = [];
let i = 0, j = 0;
while (i < left.length && j < right.length) {
if (left[i] <= right[j]) {
result.push(left[i++]);
} else {
result.push(right[j++]);
}
}
return result.concat(left.slice(i)).concat(right.slice(j));
}
pub fn merge_sort<T: Ord + Copy>(arr: &mut [T]) {
let n = arr.len();
if n <= 1 {
return;
}
let mut temp = vec![arr[0]; n]; // 预分配辅助数组
merge_sort_inner(arr, 0, n - 1, &mut temp);
}
fn merge_sort_inner<T: Ord + Copy>(
arr: &mut [T], left: usize, right: usize, temp: &mut [T],
) {
if left >= right {
return;
}
let mid = left + (right - left) / 2;
merge_sort_inner(arr, left, mid, temp);
merge_sort_inner(arr, mid + 1, right, temp);
// 如果已经有序,跳过合并
if arr[mid] <= arr[mid + 1] {
return;
}
// 复制到辅助数组
for i in left..=right {
temp[i] = arr[i];
}
let (mut i, mut j, mut k) = (left, mid + 1, left);
while i <= mid && j <= right {
if temp[i] <= temp[j] {
arr[k] = temp[i];
i += 1;
} else {
arr[k] = temp[j];
j += 1;
}
k += 1;
}
while i <= mid {
arr[k] = temp[i];
i += 1;
k += 1;
}
}