「这是我参与2022首次更文挑战的第17天,活动详情查看:2022首次更文挑战」。
记录 1 道算法题
数组中的逆序对
要求:给一个数组 [7,5,6,4],输出:5。里面有逆序对 5 对。分别是 [7,5] [6,4] [7,6] [7,4] [5,4]。
通过对每个数进行对比,可以实现暴力求解。只要用一个指针遍历,然后每次都与后面的数进行比较即可。前面的数大于后面的数就是逆序对。
那么使用了算法优化的话,要怎么实现呢?
这里要介绍一下归并排序,归并排序是一种排序算法,利用的是分治的思想,即将两个数组进行归并,每次从某个数组中拿一个。要求是拿两个数组中最小的。所以归并排序也有一个条件,那就是需要两个数组是有序数组,这样每次只需要比较数组中的第一个即可。
但是题目给了一个数组,怎么分成两个数组呢,这就是归并排序的分治思想,首先会把数组从中间分成两半,这样就有两个数组了。如果继续再切分就会得到四个数组,分裂是指数级别的增长。
一直切分的话可以使用递归,那么到最后递归的终止条件就是数组无法继续切分,就是当数组都变成一个一个的时候。然后进行合并,一级级往上返回的数组就会变成有序数组。
为什么会变成有序数组呢? 这就涉及到合并多个数组的思路了。归并是采用分组合并的方式,另外还有以一个为主数组,其他都并入的做法。相关的题目可以搜索“合并k个链表”相关的题目。
首先切到最细,一个一个数组,长度都为 1,这时候两两一组进行合并,小的排在前面,就会得到多个长度为 2 的有序数组。例如 [2,7] [5,6]
。这时候,归并排序已经可以确保这两个数组合成的数组也是有序的,[2,5,6,7]
。一层层返回的时候都进行合并,这是递归的代码。
归并排序介绍完了,那么这道题怎么解呢?这道题并不是要求排序。
其中的细节就在归并排序的过程中。 我们会对两个数组进行遍历。并且这两个数组是有序的。那么我们可以知道只要左边的数组有一个值大于右边的数组,那么意味着左边的数组剩下的数都将大于右边的数组当前的数。比如 [3,4,5] [2,6,8]
。当我们知道 3 > 2 的时候,4 必然大于 2, 5 也是。可以得到逆序对 3 个。
所以我们在归并的时候如果发现了左边的数组第 i 个大于右边的数组的某个数。那么逆序对的个数就是 左边数组.length - i
个。
那么为什么同组的不用考虑可能会有逆序对呢?
因为其实已经计算过了。在下一层的时候,即 1 * 1 组合的时候已经开始进行归并,当前看到的数组是曾经的左右两个数组。
下面是完整实现
function reversePairs(nums) {
if (nums.length <= 1) return 0
let count = 0
function merge(nums) {
// 终止条件
if (nums.length === 1) return nums
const mid = nums.length >> 1
const left = nums.slice(0, mid)
const right = nums.slice(mid)
// 递归
const a = merge(left)
const b = merge(right)
let i = 0
let j = 0
let res = []
while (i < a.length && j < b.length) {
if (a[i] <= b[j]) {
res.push(a[i])
i++
} else {
res.push(b[j])
// 计数
count += a.length - i
j++
}
}
if (i < a.length) {
for (let q = i; q < a.length; q++) {
res.push(a[q])
}
} else if (j < b.length) {
for (let q = j; q < b.length; q++) {
res.push(b[q])
}
}
return res
}
merge(nums)
return count
}