算法刷题系列(三十四):分治——拆分子问题并合并结果
算法刷题系列(三十四):分治——拆分子问题并合并结果
算法刷题系列(三十四):分治——拆分子问题并合并结果
分治不是“把输入切开”就结束。一个完整的分治算法需要三个环节:把问题拆成规模更小的同类子问题,递归求解,再用可控的合并过程得到原问题答案。归并排序能很好地展示三者如何配合。
归并排序的结构
对半切分数组,分别排序左右区间,最后线性合并。定义 sort(left, right) 的含义为:调用返回时,闭区间 [left,right] 已按升序排列。长度为零或一时本来就有序,是递归基线;两个有序子区间可以用双指针合并。
合并时,i 和 j 分别指向左右区间尚未处理的最小元素。每一步取较小者放入临时数组,直到一侧耗尽,再复制另一侧剩余元素。循环不变量是:临时数组始终是两个子区间已处理元素的有序合并。
Java 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class MergeSort {
public static void sort(int[] values) {
if (values == null) {
throw new IllegalArgumentException("values must not be null");
}
int[] buffer = new int[values.length];
sort(values, buffer, 0, values.length);
}
private static void sort(int[] values, int[] buffer, int left, int right) {
if (right - left <= 1) {
return;
}
int middle = left + (right - left) / 2;
sort(values, buffer, left, middle);
sort(values, buffer, middle, right);
merge(values, buffer, left, middle, right);
}
private static void merge(
int[] values, int[] buffer, int left, int middle, int right
) {
int i = left, j = middle, out = left;
while (i < middle && j < right) {
if (values[i] <= values[j]) {
buffer[out++] = values[i++];
} else {
buffer[out++] = values[j++];
}
}
while (i < middle) {
buffer[out++] = values[i++];
}
while (j < right) {
buffer[out++] = values[j++];
}
for (int index = left; index < right; index++) {
values[index] = buffer[index];
}
}
}
这里使用半开区间 [left,right),区间长度直接是 right - left,左右两半分别为 [left,middle) 和 [middle,right),边界不需要额外加减一。
Python 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def merge_sort(values: list[int]) -> list[int]:
if len(values) <= 1:
return values.copy()
middle = len(values) // 2
left = merge_sort(values[:middle])
right = merge_sort(values[middle:])
merged: list[int] = []
i = j = 0
while i < len(left) and j < len(right):
if left[i] <= right[j]:
merged.append(left[i])
i += 1
else:
merged.append(right[j])
j += 1
merged.extend(left[i:])
merged.extend(right[j:])
return merged
assert merge_sort([8, 3, 3, -1, 5]) == [-1, 3, 3, 5, 8]
assert merge_sort([]) == []
正确性与复杂度
基线区间长度不超过一,天然有序。假设递归返回后左右区间各自有序,合并过程每次取两侧未处理元素的较小者,因此输出保持有序且不丢失元素;合并结束后整个区间有序。由递归归纳,最终全数组有序。
递归满足 T(n)=2T(n/2)+O(n),所以时间为 O(n log n),额外数组空间为 O(n),递归栈为 O(log n)。Python 切片还会创建子数组,空间同样是线性量级,但有额外分配;若需原地控制分配,可改用索引和共享缓冲区。
常见错误与边界
- 空数组与单元素数组都应直接返回。
- 合并两段时必须复制尚未耗尽一侧的尾部。
- 不要在递归中使用含义不一致的左右边界;半开区间和闭区间任选其一,但全程统一。
- 相等时优先取左侧(
<=),归并排序便是稳定的:相等元素的原始相对顺序保留。 - 分治适用于子问题规模缩小、子问题答案可组合的情况;拆分后若仍需比较所有左右元素,合并成本可能高于线性,复杂度也会随之变化。
分析新的分治题时,可以先写出递归函数的精确定义与基线,再单独论证合并步骤的正确性,最后列出递推式。
本文由作者按照 CC BY 4.0 进行授权