vue3的快速diff之最长递增子序列

1,652 阅读4分钟

前言

大家好,之前看了《Vue.JS设计与实现》这本书,对于vue3的快速diff算法有用到最长递增子序列有点迷糊,今天来了解一下吧,leetcode300题 最长递增子序列,它主要用到了贪心+二分法查找(优化过)求值,最后移动最少的dom来更新视图的目的。

最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

例如,[0,1,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

我们知道官方的解法有两种:动态规划、贪心+二分查找

动态规划

思路:如果要求数组[2,5,3,7]的最长递增子序列,我们可以换一个思路,假设为一个行程问题,比如说从数组索引3到索引0,比作从D(7)、C(3)、B(5)、A(2)的行程规划,要求只能从大的数到小的数,走的步数越多越好;比如C不能直达到B,因为C比B小;

所以我们要算出D到A的最多步数,可以先算出C到A的最多步数,再算出D到他们的步数,D和A、B、C每一项进行比较,D大于它就在之前的基础上+1,算出D分别到A、B、C的步数,取最大值。

依次类推:先算出A到B的最多步数,再算出C到A、B的最多步数,再算出D到A、B、C的最多步数,算出每一项的最多步数,最后取里面的最大值。为什么取最大值,因为和行程问题不同,它们最后结果的起点和终点可以不是A和D。

image.png

实现:

  • 首先创建一个dp数组,长度和传入的nums数组一样,值都为1,记录每一项最长递增子序列的值。
  • 首先从数组i=1开始,比较该项nums[1]和前面项nums[0],若大于该值则取出dp[j]+1
  • 若i=n,循环比较i前面的所有项,满足j<i,找到最长的子序列 dp[i] = Math.max(dp[i], dp[j]+1)

image.png

  • i=1时,相当于计算[2,5]的最长递增子序列,判断因为2<5,所以dp[1]=dp[0]+1

  • i=2时,相当于计算[2,5, 3]的最长递增子序列,3>2,dp[2]=dp[0]+1;3<5,不处理

  • i=3时,相当于计算[2, 5,3,7]的最长递增子序列,7>2;7>5;7>3,所以要找出最大值 dp[3] = Math.max(dp[3], dp[j]+1)

/** * @param {number[]} nums * @return {number} */ 
var lengthOfLIS = function(nums) { 
    let dp = new Array(nums.length).fill(1); 
    for(let i = 1;i < nums.length; i++) { 
        for(let j = 0; j < i; j++) { 
            if(nums[i] > nums[j]) { 
                dp[i] = Math.max(dp[i], dp[j]+1) 
            } 
         }
     } 
     return Math.max(...dp) 
 };

贪心+二分查找

这个不看答案真的很难想到,用一个tail数组来存放结果

  • 首先将nums第一项存入数组,循环数组,nums[i]>tail[tail.length - 1]
    1. 就将该值push到数组里面(这样就是会使最长递增子序列的长度增加)

    2. 否则,使用二分法在tail数组中找到和nums[i]值接近的数,替换tail的这一项

  • 最后tail的长度就是最长递增子序列的值
var lengthOfLIS = function (nums) {
    let n = nums.length;
    if (n <= 1) {
        return n;
    }
    let tail = [nums[0]];//存放最长上升子序列数组
    for (let i = 1; i < n; i++) {
        if (nums[i] > tail[tail.length - 1]) {
            //当nums中的元素比tail中的最后一个大时 可以放心push进tail
            tail.push(nums[i]);
        } else {
            // 二分法查找和nums[i]值接近的数
            let left = 0;
            let right = tail.length - 1;
            while (left < right) {
                let mid = (left + right) >> 1;
                if (tail[mid] < nums[i]) {
                    left = mid + 1;
                } else {
                    right = mid;
                }
            }
            // 替换该值
            tail[left] = nums[i];
        }
    }
    return tail.length;
};

vue3diff的最长子序列

vue3采用的贪心+二分法,复杂度更低。 arr数组的值,表示在旧的childVnode对应的位置顺序。

对于let arr = [1, 5, 6, 7, 8, 2, 3, 4, 9];

贪心+二分法 的历程是:

1

1,5

1,5,6

1,5,6,7

1,5,6,7,8

1,2,6,7,8

1,2,3,7,8

1,2,3,4,8

1,2,3,4,8,9

我们发现[1,2,3,4,8,9]确实不是正确的结果,但长度是正确的;[1,5,6,7,8,9]才是正确的结果,

vue3的diff需要的,我们最后需要一个修正过程,这个时候需要一个p数组,p[i]记录的是,当前操作的result[start]的前一项 贪心+二分法 加入p数组: 括号里式对应的索引

1(0)

1(0),5(1)  新增 p=[undefined, 0]

1(0),5(1),6(2)  新增 p=[undefined, 0, 1]

1(0),5(1),6(2),7(3)  新增 p=[undefined, 0, 1, 2]

1(0),5(1),6(2),7(3),8(4)  新增 p=[undefined, 0, 1, 2, 3]

1(0),2(5),6(2),7(3),8(4)  替换 p=[undefined, 0, 1, 2, 3, 0]

1(0),2(5),3(6),7(3),8(4)  替换 p=[undefined, 0, 1, 2, 3, 0, 5]

1(0),2(5),3(6),4(7),8(4)  替换 p=[undefined, 0, 1, 2, 3, 0, 5, 6]

1(0),2(5),3(6),4(7),8(4),9(8)  新增 p=[undefined, 0, 1, 2, 3, 0, 5, 6, 4]

我们知道[0,5,6,7,4,8]索引, 对应的结果[1,2,3,4,8,9]不对

所以根据p记录的替换前的值,重建数组

最后一位是9(8)是不会算错了,我们知道最大一项对应的索引是8,所以p[8]获取的值是4,4就是它前一项的索引值;再根据p[4]获取的值是3,做为前一项的值,依次类推重建result result[5]= 8 end: p[8] === 4

result[4] = 4 end: p[4] === 3

result[3] = 3 end: p[3] === 3

result[2] = 2 end: p[2] === 2

result[1] = 1 end: p[1] === 1

result[0] = 0 end: p[0] === 0

对应的正确的索引是 [0,1,2,3,4,8] 结果[1,5,6,7,8,9]

function getSequence(arr) {
  const p = []
  const result = [0] //  存储最长增长子序列的索引数组
  let i, j, start, end, mid
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1] 
      if (arr[j] < arrI) { 
      //  如果arr[i] > arr[j], 当前值比最后一项还大,可以直接push到索引数组(result)中去
        p[i] = j //  p记录的当前位置下,前一项的索引值
        result.push(i)
        continue
      }
      // 二分法查找和arrI值接近的数
      start = 0 
      end = result.length - 1
      while (start < end) {
        mid = ((start + end) / 2) | 0
        if (arr[result[mid]] < arrI) {
          start = mid + 1
        } else {
          end = mid
        }
      }
      if (arrI < arr[result[start]]) {
        if (start > 0) {
          p[i] = result[start - 1]   // 记录当前位置下,替换位置的前一项的索引值
        }
        // 替换该值
        result[start] = i                   
      }
    }
  }
  // 通过数组p,修正最长递增子序列对应的值
  start = result.length
  end = result[start - 1]
  while (start-- > 0) {  
    result[start] = end
    end = p[end];                        
  }
  return result
}

最后

这次主要了解了一下,最长递增子序列算法题的动态规划解法和贪心和二分法解法,vue3的快速diff阶段,应用了贪心和二分法(通过优化操作保证每次子序列的顺序正确)计算最长递增子序列,因为传入的是oldChildren对应的索引,如果索引是递增的,说明dom的排列顺序不用变化;所以计算得到的递增子序列的dom是不需要再进行移动操作,做到最少的dom操作。

看到这里,如果这篇文章对你有帮助的话,欢迎大家点赞~