Vue3 diff 算法之:最长递增子序列
最长递增子序列动态规划的解法在其他博客列些,感兴趣的可以看这里动态规划:最长递增子序列
动态规划的算法复杂度是O(n^2),效率会稍微慢一些,但是相对来说比较好理解。
Vue3 的 diff 算法恰好符合 LeetCode 300. 最长递增子序列 这道题最后一个要求:
你能将算法的时间复杂度降低到 O(n log(n)) 吗?
一、Vue3 为什么需要最长递增子序列?
假设我们有 1,2,3,4,5 五个同级节点,经过移动后变成了 1,3,4,5,2。
- 我们肉眼容易看是 2 节点移动到了最后,但是如果没有最长递增子序列的概念,我们就认为是 除了 1,其他都有位置移动。
- 如果我们引入最长递增子序列的概念,我们就认为是 1,3,4,5 这四个节点是最长的递增子序列,2 节点只是在这个子序列的末尾。
- dom 操作的时候只需要做一次移动,而不是四次。
最终移动 dom 节点的核心代码:
js
//移动后, 由于 2 是最后一个节点,anchorNode 为 null 默认插入最后
parentNode.insertBefore(2, null)
二、Vue3 实现最长递增子序列
Vue3 的 diff 算法是通过贪心算法 + 二分查找 + 前驱节点记录来实现的。
- 贪心算法:只要当前遍历的数据比前一个大,加入结果数组,记录前驱节点
- 二分查找:在当前结果数组中查找比当前数据小的元素,替代当前数据,记录前驱节点
让我们模拟贪心和二分算法实现:
text
遍历移动后数组 [1, 3, 4, 5,2] 放入结果数组用于求最长子序列
[1]
[1, 3]
[1, 3, 4]
[1, 3, 4, 5]
[1, 2, 4, 5]
我们可以看到计算出来的结果长度是正确的,但是正确答案是 [1, 3, 4, 5],而我们得到的结果是 [1, 2, 4, 5]。
- 贪心算法:只要当前遍历的数据比前一个大,加入结果数组,结果数组是 [1, 3, 4, 5],但是 2 节点并不在正确答案中,我们需要辅助数组 p 来记录前驱节点。
text
遍历移动后数组 [1, 3, 4, 5,2] 放入结果数组用于求最长子序列
[1] p=[0]
[1, 3] p=[0,0] 3的前驱节点是 索引0
[1, 3, 4] p=[0,0,1] 4的前驱节点是 索引1
[1, 3, 4, 5] p=[0,0,1,2] 5的前驱节点是 索引2
[1, 2, 4, 5] p=[0,0,1,2,0] 2的前驱节点是 索引0
我们能确定的是,结果数组最后一个元素一定是正确的,因为它是最长的递增子序列。
我们根据 p 数组,倒推出正确的结果数组。
text
数组:[1, 3, 4, 5,2]
结果数组:[1, 3, 4, 5]
p 数组:[0, 0, 1, 2, 0]
遍历结果数组:
最后一个是 5, 对应的前驱索引是 2,记下一个是 4
倒推:
[4,5] 4的前驱节点是 索引1
[3,4,5]
[1,3,4,5] 得到正确答案
让我们来看看 Vue3 的实现:
js
function getSequence(arr: number[]): number[] {
// 浅拷贝数组
const p = arr.slice();
// 第一个默认为最开始
const result = [0];
let i, j, u, v, c;
// 数组长度
const len = arr.length;
// 遍历数组
for (i = 0; i < len; i++) {
//arrI 第 i 个
const arrI = arr[i];
// 如果是 0 表示新增节点
if (arrI !== 0) {
// 取出上一个结果的最后一个元素 第一次是 0
j = result[result.length - 1];
// 如果最后一个都是小于 arrI 的话,索引直接加入
if (arr[j] < arrI) {
// 记录前驱节点
p[i] = j;
// arrI 更大,直接加入结果数组
result.push(i);
// 后面步骤是二分法,只有 arrI 比 result[u] 小才更新
continue;
}
// 结果数组的开始结束索引
u = 0;
v = result.length - 1;
// 这和索引的二分法类似,只不过这里是数组元素
while (u < v) {
// 二分法查找插入位置 位运算右移,相当于 Math.floor((u + v) / 2)
c = (u + v) >> 1;
if (arr[result[c]] < arrI) {
// 如果 arrI 还要大于当前中点,左边界往右收窄
u = c + 1;
} else {
// 右边界往左收窄
v = c;
}
}
// 找到小于 arrI 的第一个元素的索引
if (arrI < arr[result[u]]) {
if (u > 0) {
// 记录前驱节点
p[i] = result[u - 1];
}
// 替换位置
result[u] = i;
}
}
}
// 倒序遍历结果数组,得到正确的结果数组
u = result.length;
// 获取最后一个元素的索引
v = result[u - 1];
while (u-- > 0) {
// 赋值当前元素的前驱节点
result[u] = v;
// 获取前驱节点
v = p[v];
}
return result;
}
TODO:补充图解