在 Vue3 Diff 算法中,有一个求解最长递增子序列长度的函数:源码链接,我想在这里以一种比较浅显易懂的方法介绍下这个算法。
- 输入:一个长度不为 0 的数组,每个索引对应一个大于等于 0 的整数
- 输出:返回其中最长严格递增子序列对应的索引列表(0 可以不计入最长递增子序列中)
- 时间复杂度:
O(nlogn)
- 空间复杂度:
O(n)
/**
* @param {number[]} arr
* @return {number[]}
*/
function getSequence(arr) {
// 新建一个数组,保存当某个索引的值为子序列最大值时前面一个值的索引
const preIndexMap = arr.slice()
// 递增子序列,默认填充第一个元素的索引
const result = [0]
// 从第二个索引开始遍历
for (let i = 1; i < arr.length; i++) {
const num = arr[i]
// 忽略 0,因为在 vue 中这里的 0 是个特殊值,代表对应新节点在旧节点列表中没有匹配节点,即新增节点
if (num === 0) continue
const maxIndex = result[result.length - 1]
if (num > arr[maxIndex]) {
// 比当前子序列最大值还大,扩充子序列
result.push(i)
// 记录索引前面的索引
preIndexMap[i] = maxIndex
} else {
// 要让序列上升的更慢,才能找到更长的递增子序列
let l = 0 // 目标是找到刚好比 num 大的值,并替换它
let r = result.length - 1
while (l < r) {
let m = l + ((r - l) >> 1)
// 因为实际输入不太可能出现相等的情况,所以没有单独做相等判断
// 否则我们可以在相等时令 l = m 提前退出循环提升算法效率
if (arr[result[m]] < num) {
// 小于 num,则肯定不能放这里,往右边继续寻找位置
l = m + 1
} else {
// 大于 num,可能为解,不过左边可能还有更合适的位置,继续循环
r = m
}
}
// 相等情况可以跳过(说明用户使用了相同的 key)
if (arr[result[l]] === num) continue
// num 可以放在 l 这个位置让子序列上升得更慢
result[l] = i
// 记录索引前面的索引
if (l > 0) {
preIndexMap[i] = result[l - 1]
}
}
}
let n = result.length
let index = result[n - 1]
while (n-- > 0) {
result[n] = index
index = preIndexMap[index]
}
return result
}
这个算法主要是贪心策略比较难理解:要让序列上升的更慢,才能找到更长的递增子序列。我们的 result
数组最开始只是作为辅助数组,每一项 result[i]
的含义是,所有长度为 i+1
的递增子序列的末尾元素中的最小值的索引。
- 假设当前辅助数组保存的递增子序列为
[2, 5]
- 如果新的
num
值为8
,那么辅助数组应该为[2, 5, 8]
,这个很好理解 - 如果新的
num
值为3
,那么应该替换5
所在的位置[2, 3]
,这样子序列就会上升的更慢- 如果下一个
num
值为4
,就能构成更长的子序列[2, 3, 4]
- 如果下一个
- 如果新的
但是辅助数组保存的不是真实的最长递增子序列,比如对于数组 [2, 5, 8, 3]
,辅助数组保存的是 [2, 3, 8]
,而实际递增子序列为 [2, 5, 8]
。
- 所以我们使用了一个数组
preIndexMap
来保存当某个索引的值为子序列最大值时前面一个值的索引,每次更新辅助数组result
时,同时更新preIndexMap
来记录真实的子序列顺序。- 我们更新
5
的时候,记录了5
前面是2
- 我们更新
8
的时候,记录了8
前面是5
- 这样当我们发现最后辅助数组最后一个元素是
8
时,就可以通过preIndexMap
找到实际的递增子序列为[2, 5, 8]
- 我们更新