vue3 diff 中的 【最长递增子系列】

2,955 阅读5分钟

前言

关于vue3 diff 已有很多相关文章,今天不讲具体diff流程,主要解刨下其中主要使用的最长递增子系列,vue3 diff 主要使用最长递增子系列来优化节点的移动

最长递增子系列(求长度)

提到最长递增子系列,必然会提到leetcode 中的 300 最长递增子系列 题目描述如下:

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

注意: 此题求解的最长递增子系列的长度 ;vue3 diff 求解的是 最长递增子系列的对应索引

代码实现

关于求最长递增子系列的长度,主要有两种思路,一是动态规划,比较好理解,但时间复杂度较高;二是贪心+二分法,时间复杂度为O(NlogN)(N: 数组的长度)

1. 动态规划

/**
* @param {number[]} nums
* @return {number}
*/
// 时间复杂度 O(N^2) : 双层遍历
// 空间复杂度 O(N) : dp table需要的空间
// 递推公式: dp[i] = Math.max(dp[i], dp[j] + 1)  (j < i && nums[i] > nums[j])【以nums[i]结尾的系列的最长递增子系列的长度】
// 由上面公式推导出:dp[i] = Math.max(dp[0, ..., i-1]) + 1  【nums从0到i-1结尾的系列的最长升序子序列长度 + 1 的最大值】

var lengthOfLIS = function(nums) {
        var len = nums.length
        if (len == 0) {
            return 0;
        }

        var dp = Array(len).fill(1)
        var max = 1
        // dp[i]: 以i结尾的最长递增子系列
        for (var i = 1; i < len; i++) {
            // 遍历i之前的元素,找到可以添加到以i结尾子系列中
            for (var j = i-1; j >= 0; j--) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1)
                }
            }
            max = Math.max(max, dp[i])
       }

        return max
};

2. 贪心 + 二分法

// 时间复杂度 O(NlogN) : 遍历 nums 列表需 O(N),在每个 nums[i]二分法需 O(logN)。
// 空间复杂度 O(N) : tails 列表占用线性大小额外空间
// tails[k] 的值代表当前(长度为 k+1)子序列的尾部元素值

// 贪心法保证子系列增长最慢,由于已排列的的系列是单调递增,所以查找当前元素插入位置可以使用二分法,效率更高

var lengthOfLIS = function(nums) {
        // 当前子系列的递增子系列数组
        const tails = Array(nums.length)
        let res = 0;
        
        for(let i = 0; i < nums.length; i++) {
          // tails初始为空,可以直接加入
           // 如果nums[i]比tails最后一个都大,直接往tails后添加nums[i];
            if (res === 0 || nums[i] > tails[res-1]) {
                tails[res++] = nums[i]
            } else {
                // 否则通过二分查找找出tails里第一个大于nums[i]的位置,并用nums[i]替换掉原来的值
                // 二分插入法: 二分法遍历tails, 找到nums[i]在tails中位置
                let l = 0, r = res;
                while(l < r) {
                    let mid = (l+r) >> 1
                    if (tails[mid] < nums[i]) {
                        l = mid + 1
                    } else {
                        r = mid
                    }
                }
                // 用nums[i]替换掉原来的值
                tails[l] = nums[i]
            }
        }
        return res
}

存在问题

由于贪心算法每次都是取当前局部最优解,可能造成的最长递增子系列在原系列中不是正确的顺序;
如:[2, 5, 3, 1], 得出结果为长度为2是正确,但是得到的实际最长递增子系列却是[1, 3], 显然这是不对的,在原系列中1在3后面

vue3 diff 中最长递增子系列(对应索引)

新节点数组在旧节点数组的索引位置,在位置数组中递增就能保证在旧数组中的相对位置的有序性,从而不需要移动,因此递增子序列的最长可以保证移动次数的最少
或者可以理解为: 新旧节点数组的最长公共子系列,在旧节点数组中除去最长公共子系列的其他节点是需要进行处理(移动、删除)

vue3 需要的不是子序列长度,也不是最终的子序列数组,而是子序列对应的索引

代码实现

// arr: 位置数组;
// 返回位置数组的递增子系列
function getSequence(arr){
 const p = arr.slice() // 拷贝一个数组 p,p[i]记录的是result在arr[i]更新前记录的上一个值,保存当前项对应的前一项的索引
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
	// 遍历位置数组
	// 排除等于 0 的情况
    if (arrI !== 0) {
          j = result[result.length - 1]
	  // (1) arrI 比 arr[j]大(当前值大于上次最长子系列的末尾值),直接添加
          if (arr[j] < arrI) {
            p[i] = j // 最后一项与 p 对应的索引进行对应, 保存上一次最长递增子系列的最后一个值的索引
            result.push(i) // result 存储的是长度为 i 的递增子序列最小末尾值的索引集合
            //(最小末尾值:要想得到最长递增子系列,需要子系列增长越慢越好,所以子系列末尾值需要最小)
            continue
	  }

          // (2) arrI <= arr[j] 通过二分查找,找到后替换它;u和v相等时循环停止
          // 定义二分查找区间[u, v]
          u = 0
          v = result.length - 1
          // 开启二分查找
          while (u < v) {
            // 取整得到当前位置
            c = ((u + v) / 2) | 0
            if (arr[result[c]] < arrI) {
              u = c + 1
            } else {
              v = c
            }
          }
        
          // 比较 => 替换, 当前子系列从头找到第一个大于当前值arrI,并替换
          if (arrI < arr[result[u]]) {
            if (u > 0) {
              p[i] = result[u - 1]  // 与p[i] = j作用一致
            }
            result[u] = i // 有可能替换会导致结果不正确,需要一个新数组 p 记录正确的结果
          }
        }
  }
  
  // 前面的逻辑与 leetcode 300 求最长子系列长度相似
  // 下面主要的修正由于贪心算法可能造成的最长递增子系列在原系列中不是正确的顺序
  u = result.length
  v = result[u - 1]
  // 倒叙回溯 用 p 覆盖 result 进而找到最终正确的索引
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}

**重点来了~~~ **

下面主要讲下vue3 如何修正贪心算法造成的最长递增子系列顺序错乱

在vue3(上文代码) 中主要通过以下实现:

  1. 依赖于在每次遍历元素时,通过遍历顺序存储当前元素在当前递增子系列中对应索引的前一个索引
p[i] = j 
// ....此处省略.....
 if (u > 0) {
   p[i] = result[u - 1]  
 }
 result[u] = i 

以上逻辑等同于p[i] = result[u-1] ==> p[arrIndex] = result[resultIndex-1]
(其中resultIndex: arr 中当前遍历元素在递增子系列中插入位置)

  1. 最终遍历完所有元素后,再分别根据之前存的对应的前索引来查找最终递增子系列当前值
  u = result.length
  v = result[u - 1]
  // 倒叙回溯 用 p 覆盖 result 进而找到最终正确的索引
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }

以上逻辑等同于 result[u-1] = p[result[u]] ==> result[resultIndex-1] = p[result[resultIndex]] = p[result[arrIndex]
(其中result[resultIndex]: arr 中当前遍历元素的 index)

// resultIndex: arr 中当前遍历元素在递增子系列中插入位置
// result[resultIndex]: arr 中当前遍历元素的 index
// result[resultIndex-1]: 最后一个比当前元素小的元素的位置
p[i] = result[u-1]  ==> p[arrIndex] = result[resultIndex-1]
result[resultIndex-1] = p[result[resultIndex]]  = p[arrIndex]
以上两个等式,刚好相互抵消,即修正之前贪心导致的位置错误

分析

假设以下分别为子节点更新前后:
        prev:    [a, b, c, d, e, f]
        next:    [c,  f,  d, b]

         索引:  [0, 1, 2, 3](位置数组中的元素索引)
位置数组:  [2, 5, 3, 1] (next数组中的元素在prev数组中的位置)

遍历arr: [2, 5, 3, 1], 每次遍历操作对应的结果如下:

arr索引(index)arr[index]递增子系列递增子系列对应的索引(result)前驱节点数组(p)
02[2][0][2, 5, 3, 1]
15[2, 5][0, 1][2, 0, 3, 1]
23[2, 3][0, 2][2, 0, 0, 1]
31[1, 3][3, 2][2, 0, 0, 1]

arr: [2, 5, 3, 1] 递增子系列对应的索引为 result: [0, 2], 前驱节点对应的索引为 p: [2, 0, 0, 1]

具体分析如下:
arr: [2, 5, 3, 1],遍历arr:
当索引index = 0时,arr[0] = 2;   p[0] = 2, 复制arr[0];  递增子系列: [2]; 对应的索引数组result = [0]
当索引index= 1时,arr[1] = 5;   由于5 > 2, 递增子系列: [2, 5]; 对应的索引数组result = [01]; resultIndex表示应该插入位置为1,所以p[1] 表示插入位置前一个,更新为p[index] = p[1] =  result[resultIndex - 1] = result[1-1]=result[0] = 0;  
当索引index= 2时,arr[2] = 3;   由于3 > 2 && 3 < 5, 递增子系列: [2, 3]; 对应的索引数组result = [02]; resultIndex表示应该插入位置为1,所以p[2] 表示插入位置前一个,更新为p[index] = p[2] =  result[resultIndex - 1] = result[1-1]=result[0] = 0;  
当索引index= 3时,arr[3] = 1;   递增子系列: [1, 3]; result = [3, 2];resultIndex表示应该插入位置为0, 小于1,  p[index] = p[3] =  result[resultIndex - 1],  所以p[3]为初始值;  

参考

leetcode 题解
www.cnblogs.com/eret9616/p/… juejin.cn/post/693724…