1370 字
4 分钟
归并排序与逆序对统计
2026-06-02

一、归并排序(1‑based 闭区间写法)#

核心框架#

void merge(int l, int mid, int r) {
vector<int> left(a + l, a + mid + 1); // 拷贝左半 [l, mid]
int i = 0, j = mid + 1, k = l;
while (i < left.size() && j <= r) {
if (left[i] <= a[j])
a[k++] = left[i++];
else
a[k++] = a[j++];
}
while (i < left.size())
a[k++] = left[i++];
// 右半剩余已在正确位置,无需处理
}
void msort(int l, int r) {
if (l >= r) return; // 长度 ≤1 终止
int mid = (l + r) / 2;
msort(l, mid);
msort(mid + 1, r);
merge(l, mid, r);
}

关键细节#

  • 递归终止条件:闭区间 [l, r] 长度为 r - l + 1l >= r 时长度为 0 或 1,直接返回。
  • 临时数组:只拷贝左半部分,右半留在原数组操作,避免被覆盖。
  • 稳定性:比较用 <=,相等时左先,保持原序。
  • 右半剩余:主循环结束后,若右半有剩余已在 a[k..r] 中正确就位(数学可证 k == j),无需再拷贝。

易错点#

错误写法正确写法原因
if (r - l <= 1) return;if (l >= r) return;1‑based 下长度为 2 时差值为 1,不应终止
while (i <= mid && j <= r) 中用 ans[i-k]ans[i-l] 或独立 iki 不同步,下标会算错
主循环用 ||&&|| 会导致一方走完后访问越界元素
merge 后不写回 a在 merge 内直接写 a[k++] = ...否则排序结果未保留

二、统计逆序对#

核心思想#

在合并两个已排序的区间时,若 left[i] > a[j],由于左半区间升序,left[i..end] 中所有元素都大于 a[j],它们都与 a[j] 构成逆序对,一次性累加 left.size() - i 个。

完整代码#

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int a[MAXN], n;
long long merge_and_count(int l, int mid, int r) {
vector<int> left(a + l, a + mid + 1);
long long inv = 0;
int i = 0, j = mid + 1, k = l;
while (i < left.size() && j <= r) {
if (left[i] <= a[j]) {
a[k++] = left[i++];
} else {
inv += left.size() - i; // 批量统计
a[k++] = a[j++];
}
}
while (i < left.size())
a[k++] = left[i++];
return inv;
}
long long count_inversions(int l, int r) {
if (l >= r) return 0;
int mid = (l + r) / 2;
long long inv = 0;
inv += count_inversions(l, mid);
inv += count_inversions(mid + 1, r);
inv += merge_and_count(l, mid, r);
return inv;
}

关键点#

  • 为什么是 left.size() - i 左半已升序,left[i] > a[j] ⇒ 从 i 到末尾的所有元素都 > a[j]
  • 数据类型:最大逆序对数 n*(n-1)/2 可达 ~5e9(n=1e5),必须用 long long
  • 相等处理:用 <= 保证相等时不统计,符合严格逆序对定义(i < ja[i] > a[j])。
  • 递归执行顺序:左子树 → 右子树 → 合并(后序遍历)。

思维进阶:为什么左右已经有序?#

递归的自底向上过程保证了有序:

[3,1,4,2] ← 合并时左右已有序
/ \
[3,1] [4,2] ← 合并时左右已有序
/ \ / \
[3] [1] [4] [2] ← 单元素天然有序

每一层的 merge 执行时,左右子区间已经通过递归完成了排序。


三、推广:处理偏序关系#

从逆序对到二维偏序#

逆序对本质是二维偏序问题:统计满足 i < ja[i] > a[j] 的点对。

  • 第一维(下标):归并分治天然保证左 < 右。
  • 第二维(值):在 merge 中比较并批量统计。

其他偏序变体#

问题条件处理方法
顺序对i < ja[i] < a[j]可在归并中统计,或转求逆序对
非严格逆序对i < ja[i] >= a[j]<= 改为 < 再统计
有条件逆序对i < ja[i] > a[j] + kmerge 中用双指针滑动满足条件的范围

CDQ 分治(三维偏序)#

处理形如 i < j, a[i] > a[j], b[i] < b[j] 的三维偏序:

  1. 按第一维排序(或分治)。
  2. 归并第二维,同时用树状数组维护第三维统计。

CDQ 分治是归并分治处理偏序的终极形态,是离线处理多维偏序问题的利器。


四、常见调试技巧#

用 cerr 跟踪递归过程#

long long count_inversions(int l, int r, int dep = 0) {
string indent(dep * 2, ' ');
cerr << indent << "进入 [" << l << "," << r << "]" << endl;
if (l >= r) { cerr << indent << "返回 0" << endl; return 0; }
int mid = (l + r) / 2;
long long inv = count_inversions(l, mid, dep+1);
inv += count_inversions(mid+1, r, dep+1);
inv += merge_and_count(l, mid, r);
cerr << indent << "返回 " << inv << endl;
return inv;
}

cerr 输出到 stderr,不影响 cout 的答案,适合调试时保留。

小数据手算验证#

[3,1,4,2] 手动模拟递归树,确保每个 merge 中的逆序对累加正确。


五、我的代码进化史(从错误到正确)#

下面记录了我从思路到手到写出正确代码的全过程,每个版本都犯过不同的错,但也一步步逼近正确答案。

版本 1:思路有了,但全是坑#

vector<int> ans;
void merge(int l, int mid, int r) {
int i = l, j = mid + 1;
while (i <= mid || j <= r) // 错误1:用 ||,越界后还在访问
{
if(a[i]<=a[j]) // 越界后 a[i] 或 a[j] 是脏数据
ans.push_back(a[i]), i++;
else
ans.push_back(a[j]), j++;
}
}
void msort(int l, int r) {
if (r - l <= 1) return; // 错误2:1-based下长度2直接返回
int mid = l + (r - l) / 2;
msort(l, mid);
msort(mid + 1, r);
merge(l, mid, r); // 错误3:merge结果存在ans,没写回a
}

问题

  • || 导致一方走完还在访问已越界的元素
  • 没有处理剩余元素(需要先用 && 循环,再用两个 while 分别收尾)
  • ans 全局变量不断追加,从未清空也从未写回原数组
  • 递归终止条件导致长度为 2 的区间无法排序

版本 2:尝试改进,引入新 bug#

void merge(int l, int mid, int r) {
int i = l, j = mid + 1;
vector<int> ans(a + l, a + mid + 1);
int k = l;
while (i <= mid && j <= r) {
if (ans[i-k] <= a[j]) // bug:i-k 的索引方式错误!
a[k++]=ans[i-k], i++;
else
a[k++]=a[j], j++;
}
}
void msort(int l, int r) {
if (r - l <= 1) return; // 同样的终止条件错误
...
}

进步:用了临时数组 vector<int> ans(a + l, a + mid + 1),思路已正确。 新 bugans[i-k]ki 变化不同步——当 else 分支执行时 k 增加但 i 不变,i-k 会变负,下标完全乱掉。

版本 3:越来越接近#

void merge(int l, int mid, int r) {
int i = l, j = mid + 1;
vector<int> ans(a + l, a + mid + 1);
int k = l;
while (i <= mid && j <= r) {
if (ans[i-l] <= a[j]) // 修正为 i-l,这次对了
a[k++]=ans[i-l], i++;
else
a[k++]=a[j], j++;
}
while(i<=mid)
a[k++] = ans[i - l]; // 左剩余处理正确
}
void msort(int l, int r) {
if (r - l <= 1) return; // 终止条件还是错的
...
}

进步ans[i-l] 是正确的索引方式。 遗留问题:递归终止条件依然没改过来。

版本 4:最终正确版本#

msort 的终止条件改为 if (l >= r) return;,所有问题解决。

关键教训#

错误类型次数如何避免
递归终止条件写错3 次闭区间 [l,r] 牢记 l>=r 才终止
临时数组索引混用 i-k1 次left 单独设下标 i,从 0 开始,不与 k 耦合
循环条件用 || 而非 &&1 次牢记:双指针先 && 走完,再单独处理剩余
merge 结果未写回1 次merge 内直接操作 a[k],不再用全局变量

这些错误看似低级,但每踩一次都在加深理解。能写出最终的正确代码,正是因为前面的每个错误都让我更清楚边界在哪。


六、心得总结#

  1. 归并排序是分治思想的典型应用:分解 → 递归 → 合并。
  2. 逆序对统计是归并排序的自然拓展:利用有序性批量统计,不增加时间复杂度。
  3. 递归执行顺序 = 后序遍历:左 → 右 → 根(合并),理解了这点递归不再抽象。
  4. 从思路到代码的桥梁
    • 先写伪代码明确每一步。
    • 注意边界条件(递归终止、指针范围)。
    • 用调试输出验证脑内模拟。
  5. 归并分治可推广到偏序问题:逆序对是入门特例,CDQ 分治是进阶方向。
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

归并排序与逆序对统计
https://caoyue.xin/posts/mergesort/
作者
Colton/曹越
发布于
2026-06-02
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录