本文是上一篇vue3中diff算法的教程的续篇!!!
如果您还不知道vue3中diff算法的基本思路,我建议您应该先去看看我上一篇的教程!!!
上一篇会提及 什么时候用到diff算法?为什么要使用diff算法?使用v-for有无key在更新过程中的区别吗?
非常详细的介绍了diff算法!!!
当然如果您只是想知道最长递增子序列的算法,那么本篇一样会给您带来帮助!!!
最长递增子序列是什么?
答:属于一种算法,在一个乱序的数值序列中,遍历这个序列,找出依次递增的数值,保存在新序列中,使得这个新序列元素的数值依次递增,并且这个新序列的长度尽可能地大(从乱序序列中找出越多越好)注意:最长递增子序列中的数值在原序列中不一定是要连续的,只要是递增的数值就符合!
注意:「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。
diff算法为什么需要用到最长递增子序列?
在上一篇手写diff算法中,在处理新乱序子级进行进行倒叙插入时,每一个乱序的元素都进行了一次插入,而某一些递增或连续的元素,根本不用动,只需要插入或移动那些位置发生改变或新增的元素;所以最长递增子序列为了找出不用动的元素,在遍历插入时,跳过这些节点的插入!!!在上一篇末尾有图解说明为什么要用到最长递增子序列的例子。
求最长递增子序列个数
序列为:[3,2,8,9,5,6,7,11,15]
情况一:3,8,9,11,15(不变的情况)
情况二:2,5,6,7,11,15 (小的替换大的,但不能回头把8插进来)
情况二是怎么得出来的?
3,2,8,9,5,6,7,11,15
在循环序列时,大的放到末尾,遇到小的值替换掉最前面大的值
3
2
2,8
2,8,9
2,5,9
2,5,6
2,5,6,7
2,5,6,7,11
2,5,6,7,11,15
在力扣题库300中的最长递增子序列和现在要写的是有区别的。力扣题中求解的是最长递增子序列的长度,我们现在写的主要是用于vue3的diff算法,最终返回的是一个映射子序列的下标数组。但核心都是一样的采用贪心算法+二分查找法
另一个区别:为了正确的符合上一篇diff算法的需求(找出最长递增子序列,这些元素将不用进行处理),在查找出最长递增子序列的下标数组后,还会对这个结果集进行特殊处理,因为这个结果集不一定是对的有弊端(对于diff算法而言,后面有例子说明),所以在写的过程中,每个子序列还会记录自己的前驱节点,最后再进行回溯,具体怎么操作,感兴趣的您一定要看下去!!!
开始
续上一篇的例子我们可以得到一个newIndexToOldIndexMap值为[5,3,4,0],那么最长递增子序列就为[3,4],由于我们在遍历新乱序子级进行倒叙插入跳过[3,4]的处理,所以结果集应该保存的是下标映射为[1,2],在遍历新乱序子级到下标为2和1的时候跳过处理,通过最少的移动实现页面渲染!
function getSequence(arr) {
let length = arr.length//拿到长度
const result = [0] //默认第0个位基准(保存是对应原序列的索引值)
let resultLastIndex //结果集中最后一个元素,在原序列中的索引值
for (let i = 0; i < length; i++) {
let arrI = arr[i]//当前数值
if (arrI !== 0) {//因为下标为0需要创建,所以不在复用范围内,需排除
resultLastIndex = result[result.length - 1] //拿到结果序列最后一项的下标(原序列)
if ( arrI > arr[resultLastIndex] ) {//比较最后一项和当前项的值,如果比最后一项大,则将当前索引放到结果集中
result.push(i)
continue
}
}
}
return result
}
getSequence([2,3,7,4,5,8,1,9])//0,1,2,5,7
i | resultLastIndex | arrI > arr[resultLastIndex] | result(初始值0,每次拿最后一项比较) |
---|---|---|---|
0 | 0 | 2 > 2? | 0 |
1 | 0 | 3 > 2? | 0 ,1 |
2 | 1 | 7 > 3? | 0 ,1,2 |
3 | 2 | 4 > 7? | 0 ,1,2 |
4 | 2 | 5 > 7? | 0 ,1,2 |
5 | 4 | 8 > 7? | 0 ,1,2 ,5 |
6 | 4 | 1 > 8? | 0 ,1,2 ,5 |
7 | 4 | 9 > 8? | 0 ,1,2 ,5,7 |
到此只能拿到普通递增的数值对应的下标,但不是最长的,在下标为3值为4时,比7小,4比7更有潜力,应该查找result序列中最前面一个比4大的进行替换,所以在result[0,1,2]中对应的是[2,3,7],数组中比4大的是7,应该把7替换掉result为[0,1,3],这样在下一轮比对5就可以插入到末尾了!
不进行替换的话result为[0,1,2,5,7],进行替换的话result为[0,1,3,4,5,7],明显替换的话长度个数更多,这就是要求出的最长递增子序列!!!
由上例子可知:我们可以在判断arrI > arr[resultLastIndex]为false:最后一项大于当前项时,利用二分查找(速度快),找出result中最前的一个比当前值大的替换掉,比如[1,5,7,8],想插入4,数组中5,7,8都比4大,但只会替换最前面的5;
思路:
//求递增子序列的个数
function getSequence(arr) {
let length = arr.length
const result = [0] //默认第0个位基准,注意这里保存的是乱序数组的下标
let start//二分查找开始位置
let end //结束位置
let middle //二分中间值
let resultLastIndex //结果集中最后一个索引值
for (let i = 0; i < length; i++) {
let arrI = arr[i]
if (arrI !== 0) {//因为下标为0需要创建,所以不在复用范围内,需排除
resultLastIndex = result[result.length - 1] //拿到最后一项进行比对
if (arrI > arr[resultLastIndex]) {//比较最后一项和当前项的值,如果比最后一项大,则将当前索引放到结果集中
result.push(i)
continue //结束此次循环
}
//arrI小于结果集最后一个元素的话,需要从结果集中找到第一个比当前值大的,然后用当前值索引进行替换,利用二分查找
//1356789 ->4 折中就是6,6比4大,往左继续折中3,3比4小,往后面只有5,替换掉5
start = 0
end = result.length - 1
while (start < end) {//start === end 就停止,在找result索引
middle = ((start + end) / 2) | 0 //取到result中间索引,二进制运算向下取整 (5/2)| 0 =2
let ArrMiddle = arr[result[middle]]//利用middle到result对应arr中的索引,再拿到arr对应的值
// 1234 ArrMiddle 6789 //中间值比当前值小就往右边走
if (ArrMiddle < arrI) {
start = middle + 1 //开始值+1,然后在下次循环再运算右边中间索引
} else { //往左边走
end = middle
}
}
//流程S0为start=0 e5为end=5 m1为middle=1
// result 1, 2, 7, 8, 9 arrI 5
// S0 e5 m2 7 < 5 ? false end = middle
// s0 e2 m1 2 < 5 ? true start = middle + 1
// s2 e2 相等退出
//result 2 4 5 8 9 10 13 18 arrI 12
//s0 e7 m3 8 < 12 ?
//s4 e7 m5 10 < 12 ?
//s6 e7 m6 13 < 12 ?
//s6 e6
//无论怎么比最后start和end一定会相等
console.log(start, end);
//找到中间值后,需要做替换操作
if (arr[result[end]] > arrI) {//end和start相等,用哪个都一样
result[end] = i //贪心法在此体现,用当前这一项,替换掉已有的比当前大的那一项,需要更有潜力的,如果不换后面更大的就无法进入
}
}
}
return result
}
console.log(getSequence([1, 2, 7, 8, 9, 5, 3, 0]));//
到此求出最长递增子序列的长度个数是对的,但利用这个结果集不满足上一篇diff算法的需求,这个结果集是有弊端的!!!
例子:
看上图,有一组乱序子序列[5,3,4,2],我们一眼就能看出CD两个元素是不需要移动的,但在我们上面的算法中2会把3给替换掉(贪心算法),所以result为[3,2],注意result保存是索引,这样就导致了C和H不用动,这3明显是错误的!
从上图可看出以上面的算法,最后数值结果为[1,3,4,6,7,9],result对应的索引为[2,1,8,4,6,7],用这个最长递增子序列去移动元素肯定是不对的,正确符合diff算法的数值结果应该是[2,3,5,6,7,9](结果可能不唯一),所以我们要进行回溯!
在回溯前,需要记录每个元素的前驱节点是谁
记录前驱流程在程序中undefined
每个数值都记录了自己的前驱,这样就可以进行回溯了,从最后一个值开始回溯,因为最后一个值肯定是最大的也是正确的
回溯流程:
i | 值 | 当前索引 | 前驱索引 | result结果集 |
---|---|---|---|---|
5 | 9 | 7 | 6 | [7] |
4 | 7 | 6 | 4 | [6,7] |
3 | 6 | 4 | 3 | [4,6,7] |
2 | 5 | 3 | 1 | [3,4,6,7] |
1 | 3 | 1 | 0 | [1,3,4,6,7] |
0 | 2 | 0 | [0,1,3,4,6,7] |
result[0,1,3,4,6,7] ,数值为[2,3,5,6,7,9],这个result才是我们要求出的最长递增子序列,resultbao保存的下标就是标识不用插入的元素。
是不是有个问题为什么不是[2,3,5,6,8,9]呢?其实是7是8并不重要,因为肯定是其中一个不用动,只是7比8小,所以把8替换掉了,通过回溯最后的结果集是正确的就行,这也就验证了结果不唯一
代码思路:
function getSequence(arr) {
let length = arr.length
const result = [0] //默认第0个位基准,注意这里保存的是乱序数组的下标
//拷贝arr,修改不会影响arr,用于节点追溯,最后要标记索引
+ const p = new Array(length).fill(0)
//console.log(p);//[0, 0, 0, 0, 0, 0, 0, 0, 0]
let start//二分查找开始位置
let end //结束位置
let middle //二分中间值
let resultLastIndex //结果集中最后一个索引值
for (let i = 0; i < length; i++) {
let arrI = arr[i]
if (arrI !== 0) {//因为下标为0需要创建,所以不在复用范围内,需排除
resultLastIndex = result[result.length - 1] //拿到最后一项进行比对
if (arrI > arr[resultLastIndex]) {//比较最后一项和当前项的值,如果比最后一项大,则将当前索引放到结果集中
result.push(i)
+ p[i] = resultLastIndex //当前放到末尾的要记住他前面的那个元素的下标是谁
continue //结束此次循环
}
//arrI小于结果集最后一个元素的话,需要从结果集中找到第一个比当前值大的,然后用当前值索引进行替换,利用二分查找
//1356789 ->4 折中就是6,6比4大,往左继续折中3,3比4小,往后面只有5,替换掉5
start = 0
end = result.length - 1
while (start < end) {//start === end 就停止,在找result索引
middle = ((start + end) / 2) | 0 //取到result中间索引,二进制运算向下取整 (5/2)| 0 =2
let ArrMiddle = arr[result[middle]]//利用middle到result对应arr中的索引,再拿到arr对应的值
// 1234 ArrMiddle 6789 //比当前值小就往右边走
if (ArrMiddle < arrI) {
start = middle + 1 //开始值+1,然后在下次循环在运算右边中间索引
} else { //往左边走
end = middle
}
}
//无论怎么比最后start和end一定会相等
// console.log(start, end);
//找到中间值后,需要做替换操作
if (arr[result[end]] > arrI) {//end和start相等,是二分查找出的那个下标,用哪个都一样
result[end] = i //贪心算法在此体现,用当前这一项,替换掉已有的比当前大的那一项,需要更有潜力的,比如7 10 13遇到 遇到9,9需要把10替换掉
+ p[i] = result[end - 1] //记住他前一个元素是谁
}
//中间值替换例子(可不看):
// arr2 3 1 5 6 8 7
// 1、 result [0 1] i 2 1>3?无法插入需要替换掉大的 end 0 result [2 1] p[0,0,undefined,0,0,0,0] 前面没有去前驱为undefined
// 2、 result [2 1 3 4 5] i 6 7>8?无法插入需要替换掉大的 end 4 result [2 1 3 4 6] p[0,0,undefined,1,3,4,4] 原本是p[0,0,undefined,1,3,4,0]
}
}
+ // console.log(p);[0, 0, undefined, 1, 3, 4, 4, 6, 1]
//通过最后一项进行回溯
+ let i = result.length
+ let last = result[i - 1]//拿到最后一项
+ // console.log(result);//为回溯result为 [ 2, 1, 8, 4, 6, 7 ]
+ while (i-- > 0) {//倒叙追溯
+ result[i] = last//最后一项肯定是去确定好的
+ last = p[last]//取出前驱索引
+ }
//回溯流程(可不看)
//数组[2, 3, 1, 5, 6, 8, 7, 9, 4] 最长数值:[1, 3, 4, 6, 7, 9]
//result[ 2, 1, 8, 4, 6, 7 ] p[ 0, 0, undefined, 1, 3, 4, 4, 6, 1 ]
// i last result p[last]
// 5 7 [ 2, 1, 8, 4, 6, 7 ] 6
// 4 6 [ 2, 1, 8, 4, 6, 7 ] 4
// 3 4 [ 2, 1, 8, 4, 6, 7 ] 3
// 2 3 [ 2, 1, 3, 4, 6, 7 ] 1 //变化:因为原本的数值4(8)替换了数值5(3)
// 1 1 [ 2, 1, 3, 4, 6, 7 ] 0
// 0 0 [ 0, 1, 3, 4, 6, 7 ] 0 //变化:因为原本的数值1(2)替换了数值2(0)
//这样最后正确数值为[2, 3, 5, 6, 7, 9 ]
return result
}
//console.log(getSequence([2, 3, 1, 5, 6, 8, 7, 9, 4]));//[ 0, 1, 3, 4, 6, 7 ]
拿到正确的最长递增子序列后就可以完善上一篇diff算法留下的问题了
const patchKeyedChildren = (oldChildren, newChildren, el) => {
....省略上面代码
//需要移动位置
//获取最长递增子序列
+ let increment = getSequence(newIndexToOldIndexMap) //不用移动的元素
//比如toBePatched4个元素乱序,倒叙3 2 1 0 ,最长递增子序列为[1,2]
//遇到下标为2,1就不要进行操作
+ let j = increment.length - 1
for (let i = toBePatched - 1; i >= 0; i--) {//倒叙插入
let index = i + s2 //找到当前元素在newChildren中的下标
let current = newChildren[index] //找到newChildren最后一个乱序元素
let anchor = index + 1 < newChildren.length ? newChildren[index + 1] : null //找到元素的下个元素
//current可能是新增的元素没有el,如果没有el
if (newIndexToOldIndexMap[i] === 0) {
patch(null, current, el, anchor)//创建元素并根据参照物插入
} else {
+ if (i != increment[j]) {//不在最长递增子序列中就要移动
+ hostInsert(current.el, el, anchor)//存在el直接根据参照物插入
+ } else {
+ j--
+ }
}
}
这样一个完整的手写diff算法就算完成了,感谢您的观看,如有不对的地方请大佬指出,万分感谢!!!