一、基础背景
去年Vue3.0正式版本推出,受到很多人的追捧。vue3.0中也对dom-diff算法进行了优化,其中就用到了 「最长递增子序列」。
先简要介绍下基础背景。我们在vue开发项目的时候,常用模板或者jsx语法来编写DOM。实际上我们编写的代码会被@vue/compiler-dom转化为虚拟 DOM 节点,即 Virtual DOM,之后再将虚拟 DOM 节点渲染成实际的 DOM 节点,Virtual DOM 也会被组织成树形结构,即 Virtual DOM 树。类似如下所示👇🏽:
也许你会有个问题,为什么要引入虚拟DOM,而不是直接把组件转换成正式DOM节点呢?在我看来有两个优点:
- 更高效: 当组件发生变化时,利用虚拟DOM树可以对新旧DOM进行比较,找出区别,只更新发生变化的DOM,让渲染更加高效。
- 更灵活: 将原来直接渲染变成先构建虚拟DOM,再进行渲染。相当于抽离出中间层,对应的渲染层可以是针对浏览器的渲染,也可以是针对小程序等其他平台的渲染。
根据上面的介绍,我们知道Vue会把我们编写的组件转换成虚拟DOM树,并且将虚拟DOM树进行比较后再根据变化情况更新真实DOM。比如原有列表为 [a, b, c, d, e, f] ,而新列表为 [a, d, b, c, e, f], 这时会这样进行diff:
-
去除相同前置和后置元素 ,此优化由 Neil Fraser 提出,可以比较容易实现而且带来带来比较明显的提升;
比如针对上情况,去除相同的前置和后置元素后,真正需要处理的是
[ b, c, d]和[d, b, c],复杂性会大大降低。
-
最长递增子序列
接着要将原数组中的
[ b, c, d]转化成[d, b, c]。Vue3 中对移动次数进行了进一步的优化。下面对这个算法进行介绍:-
首先遍历新列表,通过 key 去查找在原有列表中的位置,从而得到新列表在原有列表中位置所构成的数组。比如原数组中的
[ b, c, d], 新数组为[d, b, c],得到的位置数组为[3, 1, 2],现在的算法就是通过位置数组判断最小化移动次数; -
计算最长递增子序列
最长递增子序列是经典的动态规划算法,不了解的可以前往 最长递增子序列 去补充一下前序知识。那么为什么最长递增子序列就可以保证移动次数最少呢?因为在位置数组中递增就能保证在旧数组中的相对位置的有序性,从而不需要移动,因此递增子序列的最长可以保证移动次数的最少
对于前面的得到的位置数组
[3, 1, 2],得到最长递增子序列[1, 2],在子序列内的元素不移动,不在此子序列的元素移动即可。对应的实际的节点即d节点移动至b, c前面即可。
-
本文就将介绍最长递增子序列的算法。大家可以配合着Vue3中的源码来阅读,希望读完这篇文章后能够更加理解Vue3中关于最长递增子序列的实现。
Vue3.0的源码👉:vue-next/renderer.ts 请阅读其中的getSequence方法。
力扣上没有完全一样的题目,有一道最接近的题 300. 最长递增子序列 ,它的要求是求最长递增子序列的长度,我们可以把题目换成求最长递增子序列的索引。
二、题目描述:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18] 输出:[ 2, 4, 5, 7 ] 解释:最长递增子序列是 [2,3,7,18],因此索引是[ 2, 4, 5, 7 ]。
这里再强调一遍,我们要求的是最长子序列对应的索引。
示例 2:
输入:nums = [0,1,0,3,2,3] 输出:[ 0, 1, 4, 5 ]
解释:最长递增子序列是 [0,1,2,3],因此索引是[ 0,1,4,5 ]。
示例 3:
输入:nums = [7,7,7,7,7,7,7] 输出:[0]
解释:最长递增子序列是 [7],因此索引是[0]。
敲黑板,👩🏫,我们要求的是最长递增子序列的索引。ok,强调完了,开始解题吧。
三、思路分析:
假设我们要实现getSequence方法,入参是nums数组,返回结果是一个数组。
/**
* @param {number[]} nums
* @return {number[]}
*/
var getSequence = function (nums) {
// your code
}
步骤1. 先创建一个空数组result保存索引。遍历nums,将当前项current和result的最后一项对应的值last进行比较。如果当前项大于最后一项,直接往result中新增一项;否则,针对result数组进行二分查找,找到并替换比当前项大的那项。下图示意图中为了方便理解result存放的是nums中的值,实际代码存放的是数组索引。
步骤2. 这步是难点,因为步骤1在替换的过程中
贪心了,导致最后的结果错乱。
为了解决这个问题,使用的前驱节点的概念,需要再创建一个数组preIndexArr。在步骤1往result中新增或者替换新值的时候,同时preIndexArr新增一项,该项为当前项对应的前一项的索引。这样我们有了两个数组:
-
result:[1,3,4,6,7,9]
-
preIndexArr:[undefined,0,undefined,1,3,4,4,6,1]
result的结果是不准确的,但是result的最后一项是正确的,因为最后一项是最大的,最大的不会算错。我们可知最大一项是值9,索引是7。可查询preIndexArr[7]获得9的前一项的索引为6,值为7...依次类推能够重建新的result。
注意:下图中为了方便理解,result存放的是值,实际代码中存放的是索引。
ok,思路说完了,那么下面就开始码代码了🧑💻
步骤1
var getSequence1 = function (nums) {
let result = []
for (let i = 0; i < nums.length; i++) {
let last = nums[result[result.length - 1]],
current = nums[i]
if (current > last || last === undefined) {
// 当前项大于最后一项
result.push(i)
} else {
// 当前项小于最后一项,二分查找+替换
let start = 0,
end = result.length - 1,
middle
while (start < end) {
middle = Math.floor((start + end) / 2)
if (nums[result[middle]] > current) {
end = middle
} else {
start = middle + 1
}
}
result[start] = i
}
}
return result
}
步骤2
var getSequence = function (nums) {
let result = [],
preIndex = []
for (let i = 0; i < nums.length; i++) {
let last = nums[result[result.length - 1]],
current = nums[i]
if (current > last || last === undefined) {
// 当前项大于最后一项
preIndex[i] = result[result.length - 1]
result.push(i)
} else {
// 当前项小于最后一项,二分查找+替换
let start = 0,
end = result.length - 1,
middle
while (start < end) { // 重合就说明找到了 对应的值,时间复杂度O(logn)
middle = Math.floor((start + end) / 2)// 找到中间位置的前一个
if (nums[result[middle]] > current) {
end = middle
} else {
start = middle + 1
}
}
// 如果相同 或者 比当前的还大就不换了
if (current < nums[result[start]]) {
preIndex[i] = result[start - 1] // 要将他替换的前一个记住
result[start] = i
}
}
}
// 利用前驱节点重新计算result
let length = result.length, //总长度
prev = result[length - 1] // 最后一项
while (length-- > 0) {// 根据前驱节点一个个向前查找
result[length] = prev
prev = preIndex[result[length]]
}
return result
}
小结:
-
本题目用到的核心思想有:贪心,二分查找,前驱节点
-
这个题解的时间复杂度为
O(nlogn),即for循环的n乘以二分查找的logn
四、完整代码:
在 三、思路分析的步骤2已给出完整代码
五、总结:
本文介绍了300. 最长递增子序列的其中一种解题思路,核心思想用到了贪心,二分查找和前驱节点。
vue3.0中核心dom-diff算法用到了最长递增子序列,学习掌握此题的解法有助于我们更好的了解vue3.0的dom-diff。
感谢阅读~
——
本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情