1 实现思路
1 最容易想到的暴力解法是,通过双层遍历一一比较 arr[i]和 arr[i+1~ end]的数值,是逆序对就计数
2 从暴力解法可以看出,要统计逆序对数量,本质上: 成员A一定是需要 和所有非成员A 一一进行比较的
- a: 归并排序本质上,也会在mergePair这一步,让所有成员都进行一一对标,比如
a,b,c,d,e,f,g,h
↙ ↘
abcd efgh
↙ ↘ ↙ ↘
ab cd ef gh
↙↘ ↙ ↘ ↙ ↘ ↙ ↘
a b c d e f g h
开始排序并返回1: a和b会进行比较
ba dc ef hg
开始排序并返回2: a和b已经比较过 + a会和dc进行对比
bacd efgh
开始排序并返回3: a和bdc已经比较过 + a会和efgh进行对比
abcdefgh
也就是说: a会和除了和a本身的 其他所有元素进行比较
- b: 归并的第2个特点是: 他的每次归并过程,都会返回有序的数组成员,这就意味着我们可以减少比较次数
如果 bacd, efgh的第一轮比较循环中,b比e大,那么比b大的 acd也一定大于e,不用再让 acd和e 显式比较了
2 代码实现
1 方法1: 归并排序 时间复杂度 O(nlogn); 空间复杂度O(n)
let copy = [];
function reversePairs(record) {
copy = new Array(record.length);
return mergeSort(record, 0, record.length - 1);
}
// 归并排序子过程
function mergeSort(arr, l, r) {
if (l >= r) return 0;
const mid = l + ((r - l) >> 1);
// 易错点1: 要明确 递归分治的返回值含义,按实际要求明确清楚 递归函数返回值
const count1 = mergeSort(arr, l, mid);
const count2 = mergeSort(arr, mid + 1, r);
// 优化点1: 说明必然是有序(升序)情况,本轮无需再进行 左右区间归并
if (arr[mid] <= arr[mid + 1]) return count1 + count2;
// 否则除了左右子区间逆序对数量,还要计算 左右区间合并时的逆序对数量
return mergePart(arr, l, mid, r) + count1 + count2;
}
function mergePart(arr, l, mid, r) {
let res = 0;
// 易错点2.1: 这里不能使用copy = arr.slice(l, r+1),因为这样会形成索引位置偏移
// 即 recode[5~9]会偏移为copy[0,4],导致后续从copy[l/mid]取值比较时,会形成空值比较
// 导致 record数组内容错误 && res值计算错误
// 易错点2.2: 这里不能使用copy = [...arr], 因为存在用例会导致超时
for (let i = l; i <= r; i++) {
copy[i] = arr[i];
}
let p1 = l, p2 = mid + 1, p = l;
while (p1 <= mid && p2 <= r) {
if (copy[p1] <= copy[p2]) {
arr[p++] = copy[p1++];
} else {
arr[p++] = copy[p2++];
// 易错点3.1: 这里应该是 res += mid - p1 + 1; 而不是 res+= 1
// 因为 在这种情况下,copy[p1~mid]必然 都会大于 copy[p2]
// 且索引是从0开始,所以其对应数量就是 mid - i + 1, 而不仅仅是当前p1这一个
// 易错点3.2: 之所以是+=,是因为该次比较完成后,会继续进入下一次符合的 while循环
// 所以需要累计 逆序对数量
res += mid - p1 + 1;
}
}
while (p1 <= mid) arr[p++] = copy[p1++];
while (p2 <= r) arr[p++] = copy[p2++];
return res;
}