手写diff算法之最长递增子序列vue3(例子+图解+详细注释)

本文是上一篇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
28
289
259
256
2567
256711
25671115

前言介绍

在力扣题库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

iresultLastIndexarrI > arr[resultLastIndex]result(初始值0,每次拿最后一项比较)
002 > 2?0
103 > 2?0 ,1
217 > 3?0 ,1,2
324 > 7?0 ,1,2
425 > 7?0 ,1,2
548 > 7?0 ,1,2 ,5
641 > 8?0 ,1,2 ,5
749 > 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;
思路:

  1. 添加三个属性start(开始索引)、end (结束值索引)、middle(中间索引),while遍历start === end 就停止
  2. 每次循环都取出一个中间索引middle,并取出对应的数值,利用这个中间值和当前值arrI比较
  3. 中间值小于当前值的话就往右边继续折半查找,否则往左边继续折半查找 找到中间值后做替换操作
  4. while结束会有一个中间索引,利用这个索引拿到数值再进行判断,如果这个中间值大于当前值就进行替换掉对应下标
//求递增子序列的个数
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

  1. 放入2没有前驱为undefined (2)
  2. 放入3要记录2为前驱,所以记录2的索引0(2,3)
  3. 1要替换2,2没有前驱,所以1的前驱也为undefined (1,3)
  4. 放入5,5的前驱为3,所以记录3的索引1 (1,3,5)
  5. 放入6,6的前驱为5,所以记录5的索引为3(1,3,5,6)
  6. 放入8,8的前驱为6,所以记录6的索引为4(1,3,5,6,8)
  7. 7要替换掉8,8的前驱为6,所以记录6的索引为4(1,3,5,6,7)
  8. 放入9,9的前驱为7,所以记录7的索引为6(1,3,5,6,7,9)
  9. 4要替换掉5,5的前驱为1,所以记录5的索引为1(1,3,4,6,7,9)

每个数值都记录了自己的前驱,这样就可以进行回溯了,从最后一个值开始回溯,因为最后一个值肯定是最大的也是正确的
回溯流程

i当前索引前驱索引result结果集
5976[7]
4764[6,7]
3643[4,6,7]
2531[3,4,6,7]
1310[1,3,4,6,7]
020[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替换掉了,通过回溯最后的结果集是正确的就行,这也就验证了结果不唯一

代码思路:

  1. 新增一个和乱序数组一样长的数组,用于映射保存前驱索引
  2. 在if (arrI > arr[resultLastIndex])判断当前值大于最后值进行push时,进行记录最后值的索引
  3. 在if (ArrMiddle < arrI)中间值大于当前值,需要替换掉中间值,并保存中间值的前驱索引
  4. 最后对result进行倒叙回溯(通过最后一项)
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算法就算完成了,感谢您的观看,如有不对的地方请大佬指出,万分感谢!!!

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值