本篇比较适合对Vue3的快速diff算法有了解的,但是不明白Vue3中最长递增子序列实现的朋友阅读。本篇只阐述个人对求最长递增子序列部分的理解。
为什么求最长递增子序列?
减少dom的移动,从而减少重排。如果更新节点时,只是先卸载旧dom,挂载新dom,会导致下面的dom全部进行重排。
怎么求最长递增子序列?
快速diff要求拿到的最长递增子序列是旧节点在旧children
中的索引,首先要知道这一点。 其实求最长递增子序列的方法好几种,比如动态规划贪心加二分等,这里为了说清楚快速diff,先介绍另一种方法,快速diff其实就是对它的优化。
快速diff前的小插曲:
可以去b站看渡一讲的最长递增子序列的视频!有动画,比看这段文字好容易理解多了! 假如要求一个数组[2, 4, 5, 3, 1, 1, 8, 123, 99]
的最长递增子序列。
这个方法的核心是 保存最长递增子序列长度为1,2,3.....时的最优解。 这里的最优解是,当最长递增子序列长度为1,2,3......时序列最有可能增长的解。比如这个例子中当遍历到1时,长度为1的最优解就是1了,而不是2。这样最后一种情况就是最终的结果!!!
那为什么要保存长度的这么小的情况呢?因为不知道后面有多少节点,由于长度比较小的情况,它们最后一个节点的值也比较小,它的增长潜力更大。长度长的虽然最后一个节点值比较大,增长潜力小,但是它们长,也更可能成为结果,所以也应该保存。
遍历的过程中就是要维护这些情况。那么怎么维护呢?其实维护操作就分两种情况:
- 遍历存储的这些情况,如果当前节点比最后一个节点大,那么它可以增长。若这个情况目前是最长的,则新增一个情况;不是的话,用目前节点和下一个情况(就是长度+1的情况)的最后一个节点比较,若是小的话,就可以更新它了。
- 如果它比长度为1的情况小,那么直接更新长度为1的情况。
js
const getLIS = (nums) => {
const result = [0, [nums[0]]];
for (let i = 1; i < nums.length; i++) {
for (let j = 1; j < result.length; j++) {
const curEnd = result[j][j - 1];
if (nums[i] > curEnd) {
const nextEnd = j + 1 < result.length ? result[j + 1][j] : null;
if (nextEnd && nextEnd > nums[i]) {
result[j + 1][j] = nums[i];
} else if (!nextEnd) {
result[j + 1] = [...result[j], nums[i]];
}
} else if (nums[i] < curEnd && j === 1) {
result[1][0] = nums[i];
}
}
}
console.log(result);
};
getLIS([2, 4, 5, 3, 1, 1, 8, 123, 99]);
快速diff实现:
快速diff其实上面的一个优化。上面保存了长度为所有可能情况下的结果,占用内存比较大,而且当最长递增子序列比较长的时候,两层for循环比较慢。 快速diff实际上只保存了长度为1,2,3......情况下最后一个节点的索引。 假如这个容器为result
吧。
但是需要注意的是,遍历完后,result
最后一个节点的含义是不是 长度为result.length
时,最长递增子序列最后一个节点的索引,它就是最终要求的最长递增子序列的最后一索引。
那么有没有一种方法可以通过这个确定索引将最终结果还原?结合上面条件,是不是通过确定的最后一个节点找到前一个节点,再找前一个节点......就可以完成了?!
我们知道,以当前节点结尾的最长递增子序列 = 前面最后一个节点的值比它小的 最长递增子序列 + 当前节点
,这就是一个递推公式,最终的最长递增子序列也是逐渐递推出来的!
我们的result
最后一个节点还有一层含义:它是以source[result[result.length - 1]]
结尾的最长递增子序列的最后一个节点。它是不是可以通过前一个节点为结尾的最长递增子序列推出来 + 1给推出来?前一个结节点是不是也可以以同样的方法推出来?
所以prevs
只要记录 在以某个节点为结尾的最长递增子序列中,这个节点的前一个节点就可以了。
那么怎么求prevs
呢?在遍历时,我们先使用二分找到第一个比它大的节点进行替换,那么它和前面所有节点就构成了以它为结尾的最长递增子序列 ,那么prevs[i]
就是被替换节点前面那个节点。
基本的理论搞完了,下面贴上我的代码,仅供参考!
js
const getLis = (source) => {
const result = [];
const prevs = Array.from(source); // 记录以source[i]结尾的最长递增子序列,source[i]前一个节点的索引
for (let i = 0; i < source.length; i++) {
const replacedIdxInResult = bs(result, source, 0, result.length - 1, source[i]);
if (replacedIdxInResult !== -1) {
prevs[i] = result[replacedIdxInResult - 1] || -1;
result[replacedIdxInResult] = i;
} else {
prevs[i] = result.length === 0 ? -1 : result[result.length - 1];
result.push(i);
}
}
const finalRes = [];
const endIdxInSource = result[result.length - 1];
finalRes.push(endIdxInSource);
let prevIdxInSource = prevs[endIdxInSource];
while (prevIdxInSource >= 0) {
fs.push(pinalRerevIdxInSource);
prevIdxInSource = prevs[prevIdxInSource];
}
return finalRes.reverse();
};
const bs = (result, source, l, r, num) => {
while (l < r) {
let mid = Math.floor((l + r) >> 1);
if (source[result[mid]] > num) {
r = mid;
} else {
l = mid + 1;
}
}
if (source[result[l]] > num) return l;
else return -1;
};