Vue3之最长递增子序列

129 阅读4分钟

简介

最长递增子序列在Vue2和Vue3的源码中都有该问题的算法,通过找到最长的递增子序列来达到减少dom操作更新视图的目的 而Vue3在diff算法的时候,采用了更加高效的算法,进行最少的dom移动操作达到视图更新的目的

相关问题

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

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入: nums = [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长递增子序列是 [2,3,7,101],因此长度为 4 

这是leetcode的问题leetcode.cn/problems/lo…

动态规划

该运算比较好理解,复杂度为O(n²)

思路:如果要求一个数组nums的最长递增子序列,那就反着推,最后一个数的最大序列是从前面所有比它小的数字中选一个最长的+1,依次往前类推,就能得到答案

实现:创建一个dp数组,长度跟nums数组相同,初始化里面的值都为1,表示每一个数字最少都是1个长度; 然后循环nums,从nums的前i数组中利用Math.max函数获取max,dp[i] = max + 1; 最后返回dp中最大值就是最长递增子序列的长度

function lengthOfLIS(nums: number[]): number {
    const n = nums.length
    if (n <= 1) return n
    // 设置一个dp数组,其中存放的是跟nums长度相同的,初始化为1的数组
    const dp = Array.from({length: n}, () => 1)

    for(let i = 1; i< n;i++) {
        let max = 0
        for(let j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                max = Math.max(max,dp[j])
            }
        }
        dp[i] = max + 1
    }
    return Math.max(...dp)
};

在做解题的时候,注意数组nums中可能有相同的值

贪心+二分查找获取长度

这个是目前Vue3所采用的策略,比较难理解,复杂度为O(nlogn)

思路:在一个新的dp数组中依次加入nums的数字,如果加入的数字大于dp数组最后一个数字,那么就直接加入,否则,就二分找到离这个数最近的大于这个数的数字的位置,然后当前数字替换这个大点的数字,最后返回这个dp数组的长度即可

实现:通过在dp数组中一直新增或者替换元素,最终dp元素的长度就是最长递增长度,但是dp数组中的元素不一定是正确数组

function lengthOfLIS(nums: number[]): number {

    const n = nums.length
    if (n <= 1) return n
    const dp = [nums[0]]

    for(let i = 1;i< n;i++) {
        const cur = nums[i]
        if (cur > dp[dp.length - 1]) {
            dp.push(cur)
        } else if (cur < dp[dp.length - 1]) {
            // 二分替换最近的右边元素
            let left = 0
            let right = dp.length - 1
            while(left < right) {
                const mid = (left + right) >> 1
                if (cur > dp[mid]) {
                    left = mid + 1
                } else if (cur < dp[mid]){
                    right = mid
                } else {
                    left = mid
                    right = mid                    
                }
            }
            dp[left] = cur
        }
    }
    return dp.length
};

贪心+二分查找获取正确的数组

这个问题解法比较复杂

个人理解:p数组用来记录索引,用一个p数组,来记录每次操作dp数组的时候,新增或者替换的元素的前一个元素在nums中的索引值, 比如是[undefined,1,3,5],然后在根据p数组,来修正dp数组,得到正确的最长递增子序列数组; tail数组记录贪心+二分之后的数字,tail的长度是准确的,但是里面记录的数字不一定正确 所以有了p数组之后,tail的最后一个数字的索引是正确的,因为最后一位肯定是正确的,然后从最后一位往前推导,用最后一位数组的下标值index在p中获取前一位的下标值index2=p[index],然后tail[倒数第二位]=nums[index2],然后再通过index2找到index3=p[index2],然后tail[倒数第三位]=nums[index3],依次类推,碰到undefind代表已经替换完正序第一位了,就直接结束返回tail

function lengthOfLIS(nums: number[]): number[] {
  const n = nums.length;
  if (n <= 1) {
    return n;
  }
  const p = [];

  const tail = [0];
  for (let i = 1; i < n; i++) {
    const cur = nums[i];
    if (cur > nums[tail[tail.length - 1]]) {
      tail.push(i);
      p[i] = tail[tail.length - 2];
    } else if (cur < nums[tail[tail.length - 1]]) {
      // 二分法在tail里找到 第一个大于cur的值
      let left = 0;
      let right = tail.length - 1;
      while (left < right) {
        const mid = (left + right) >> 1;
        if (cur < nums[tail[mid]]) {
          right = mid;
        } else if (cur > nums[tail[mid]]) {
          left = mid + 1;
        } else {
          left = mid;
          right = mid;
        }
      }
      tail[left] = i;

      p[i] = tail[left - 1];
    }
  }

  let start = tail.length;
  let end = tail[start - 1];
  while (start-- > 0) {
    if (end != undefined) {
      tail[start] = end;
      end = p[end];
    } else {
      start = -1;
    }
  }

  return tail;
}