← 目录 / 算法文档 · 模块九 分治与排序 / 9.1 分治

9.1 分治

把大问题一分为二,各自解决,再合并结果——分而治之。归并排序和快速排序都是分治的经典应用。

本页目录
① 分治的核心思想

分治(Divide and Conquer)由三个步骤组成:

分治三步骤
① 分(Divide)
把原问题拆成若干规模更小、结构相同的子问题。通常从中间切开,一分为二。
② 治(Conquer)
递归地解决每个子问题。当子问题小到可以直接处理时(如只有 1 个元素),直接返回。
③ 合(Combine)
把子问题的解合并成原问题的解。这一步往往是分治算法中最有技巧、最关键的部分。

分治的威力在于:把一个 O(n²) 的问题,通过"分成两半、各自解决"变成 O(n log n)。每次对半切,只需要 log₂n 层,每层合并的总工作量是 O(n),整体就是 O(n log n)。

② 归并排序:分治的教科书示例

归并排序是最标准的分治算法:

归并排序的三步对应
从数组中间切一刀,左半段和右半段各自独立处理
递归地对左半段排序,递归地对右半段排序(子问题和原问题结构相同)
把两段已排好序的数组合并成一段有序数组——双指针比大小,依次取小的放入结果
③ 归并排序图解

以数组 [5, 2, 8, 1, 9, 3] 为例,完整展示分裂和合并的过程:

原数组 5 2 8 1 9 3 分裂 5 2 8 1 9 3 5 2 8 1 9 3 → 2 8(已序) → 1 9(已序) 合并 合并 2 5 8 1 3 9 最终合并 1 2 3 5 8 9
灰色箭头向下是"分",绿色箭头向上是"合"。每层的合并操作是线性扫描两个指针,总量 O(n);共 log n 层,总复杂度 O(n log n)。
④ 归并排序代码
C++ · 归并排序完整实现
1int a[MAXN], tmp[MAXN]; // tmp 是合并时的临时数组
2
3void merge_sort(int l, int r) {
4 if (l >= r) return; // 边界:只有 1 个元素,无需排序
5
6 int mid = (l + r) / 2;
7 merge_sort(l, mid); // ① 分:递归排左半段
8 merge_sort(mid+1, r); // ① 分:递归排右半段
9
10 // ③ 合:双指针合并两段有序数组
11 int i = l, j = mid+1, k = l;
12 while (i <= mid && j <= r)
13 tmp[k++] = (a[i] <= a[j]) ? a[i++] : a[j++];
14 while (i <= mid) tmp[k++] = a[i++]; // 左段剩余
15 while (j <= r) tmp[k++] = a[j++]; // 右段剩余
16 for (int x = l; x <= r; x++) a[x] = tmp[x]; // 写回原数组
17}
18
19// 调用方式
20merge_sort(1, n); // 对 a[1..n] 排序
💡
合并的关键:两个指针 ij 分别指向左段和右段的当前位置,每次把更小的那个放进 tmp,直到某段用完后把另一段剩余元素直接追加进去。合并操作是线性的 O(n),这是归并排序效率的保证。
⑤ 快速排序:另一种分治思路

快速排序也是分治,但与归并排序"先分后合"的思路相反——它先处理再递归:选一个基准值(pivot),把数组分成"比它小的"和"比它大的"两部分,再分别递归。合并步骤几乎不需要额外工作。

一次 partition 的效果(pivot = 5)
操作前
5
pivot
2
8
1
9
3
partition 后
2
< 5
1
< 5
3
< 5
5
pivot
8
> 5
9
> 5
5 就位了!左右再各自递归
pivot(红色)经过一次 partition 后,落在它最终应该在的位置上——左边都比它小,右边都比它大。此后只需递归处理左右两段,不需要额外的合并步骤。
C++ · 快速排序
1void quick_sort(int l, int r) {
2 if (l >= r) return;
3 int pivot = a[(l+r)/2]; // 取中间元素作基准(降低最坏情况概率)
4 int i = l - 1, j = r + 1;
5 while (i < j) {
6 do i++; while (a[i] < pivot); // 找左边第一个 ≥ pivot 的
7 do j--; while (a[j] > pivot); // 找右边第一个 ≤ pivot 的
8 if (i < j) swap(a[i], a[j]); // 交换,使左小右大
9 }
10 quick_sort(l, j); // 递归左段
11 quick_sort(j+1, r); // 递归右段
12}
⑥ 逆序对:分治的延伸应用

逆序对是指数组中满足 i < ja[i] > a[j] 的数对。暴力枚举所有对是 O(n²),利用归并排序的合并过程可以在 O(n log n) 内统计完——合并时,每次右段的元素比左段当前元素小,说明它与左段剩余元素构成若干逆序对

C++ · 归并排序顺手统计逆序对
1long long ans = 0;
2
3void merge_sort(int l, int r) {
4 if (l >= r) return;
5 int mid = (l + r) / 2;
6 merge_sort(l, mid);
7 merge_sort(mid+1, r);
8 int i = l, j = mid+1, k = l;
9 while (i <= mid && j <= r) {
10 if (a[i] <= a[j]) tmp[k++] = a[i++];
11 else {
12 ans += mid - i + 1; // 左段 i..mid 都比 a[j] 大,都构成逆序对
13 tmp[k++] = a[j++];
14 }
15 }
16 while (i <= mid) tmp[k++] = a[i++];
17 while (j <= r) tmp[k++] = a[j++];
18 for (int x=l;x<=r;x++) a[x]=tmp[x];
19}
🏆
逆序对是 CSP-J / NOIP 的经典考点,几乎必考。归并排序统计逆序对是标准解法,务必掌握。关键只有一行:当右段元素 a[j] 比左段 a[i] 小时,左段从 i 到 mid 的所有元素都与 a[j] 构成逆序对,一次性加上 mid - i + 1
⑦ 分治与递归的关系
对比项归并排序快速排序
分的方式从中间对半切,左右大小相等按 pivot 值分,两侧大小不定
关键步骤合并(Combine)——双指针合并划分(Partition)——双指针交换
时间复杂度O(n log n) 稳定平均 O(n log n),最坏 O(n²)
空间复杂度O(n),需要临时数组O(log n) 递归栈,原地排序
稳定性稳定(相等元素相对顺序不变)不稳定
竞赛适用需要稳定排序、统计逆序对时用通用排序首选(或直接用 sort)
📖
本节小结
・分治 = 分(Divide)+ 治(Conquer)+ 合(Combine),通过对半切把 O(n²) 降到 O(n log n)。
・归并排序:先递归排序,再双指针合并——合并是关键,顺手可统计逆序对。
・快速排序:先 partition(以 pivot 为界分左右),再递归——原地操作,常数更小。
・竞赛中排序一般直接用 sort,但归并统计逆序对是必须手写的经典题型。