一、基础背景
vue3.0中也对dom-diff算法进行了优化,其中就用到了 「最长递增子序列」。
在项目中,实际上我们编写的模板或者jsx语法会被@vue/compiler-dom
转化为虚拟 DOM 节点,即 Virtual DOM,之后再将虚拟 DOM 节点渲染成实际的 DOM 节点,Virtual DOM 也会被组织成树形结构,即 Virtual DOM 树。类似如下所示:
<div id="app">
<p class="text">hello world!!!</p>
</div>
对应的Virtual DOM
{
tag: 'div',
props: {
id: 'app'
},
chidren: [
{
tag: 'p',
props: {
className: 'text'
},
chidren: [
'hello world!!!'
]
}
]
}
也许你会有个问题,为什么要引入虚拟DOM,而不是直接把组件转换成正式DOM节点呢?
- 具备跨平台的优势
由于虚拟DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等,是实现ssr、小程序等的基础。
- 提升渲染性能。
因为DOM是一个很大的对象,直接操作DOM,即便是一个空的 div 也要付出昂贵的代价,执行速度远不如我们抽象出来的 Javascript 对象的速度快,因此,把大量的DOM操作搬运到 Javascript 中,运用diff算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。虚拟DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
- 虚拟DOM的应用
渲染函数:https://cn.vuejs.org/v2/guide/r渲染函数 & JSX | Vue.jshttps://cn.vuejs.org/v2/guide/r
根据上面的介绍,我们知道Vue会把我们编写的组件转换成虚拟DOM树,并且将虚拟DOM树进行比较后再根据变化情况更新真实DOM。比如原有列表为 [a, b, c, d, e, f]
,而新列表为 [a, d, b, c, e, f]
, 这时会这样进行diff:
-
去除相同前置和后置元素 即双端Diff,可以比较容易实现而且带来带来比较明显的提升;
比如针对上情况,去除相同的前置和后置元素后,真正需要处理的是
[ 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.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]。
三、思路分析:
假设我们要实现getSequence方法,入参是nums数组,返回结果是一个数组。
/**
* @param {number[]} nums
* @return {number[]}
*/
var getSequence = function (nums) {
// your code
}
步骤1
先创建一个空数组result保存索引。遍历nums,将当前项current
和result的最后一项对应的值last
进行比较。如果当前项大于最后一项,直接往result中新增一项;否则,针对result数组进行二分查找,找到并替换比当前项大的那项。下图示意图中为了方便理解result存放的是nums中的值,实际代码存放的是数组索引。
function getSequence (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
这步是难点,因为步骤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,思路说完了,那么下面就开始码代码了
function getSequence(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) {
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