Skip to content
字数
1888 字
阅读时间
9 分钟

最长递增子序列

思路:

js
1. 沿用动态规划的 `dp` 数组`dp[i]` 表示以 `nums[i]` 结尾的 LIS 长度)。
2. 新增 `prev` 数组`prev[i]` 记录 `nums[i]` 在 LIS 中的前一个元素索引用于回溯子序列)。
3. 找到 `dp` 数组中的最大值及其索引后通过 `prev` 数组反向追溯还原出最长递增子序列
function findLIS(nums) {
  if (nums.length === 0) return [];

  const n = nums.length;
  const dp = new Array(n).fill(1); // dp[i]:以nums[i]结尾的LIS长度
  const prev = new Array(n).fill(-1); // prev[i]:记录前一个元素的索引
  let maxLen = 1;
  let maxIndex = 0; // 最长子序列的结尾索引

  for (let i = 1; i < n; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[j] < nums[i] && dp[j] + 1 > dp[i]) {
        dp[i] = dp[j] + 1;
        prev[i] = j; // 记录前一个元素的位置
      }
    }
    // 更新最长长度和对应的结尾索引
    if (dp[i] > maxLen) {
      maxLen = dp[i];
      maxIndex = i;
    }
  }

  // 回溯构建最长子序列
  const lis = [];
  while (maxIndex !== -1) {
    lis.unshift(nums[maxIndex]); // 从后往前插入
    maxIndex = prev[maxIndex];
  }

  return lis;
}

// 示例
console.log(findLIS([10, 9, 2, 5, 3, 7, 101, 18])); // 输出:[2, 3, 7, 101] 或 [2, 5, 7, 18](取决于回溯路径)

#### 思路

1. 沿用贪心算法的 `tails` 数组存储长度为 `i+1` 的 LIS 末尾元素最小值)。
2. 新增 `indices` 数组记录 `tails` 中每个元素在原数组 `nums` 中的索引`prev` 数组记录每个元素在 LIS 中的前一个元素索引
3. 遍历过程中通过二分查找确定元素位置并同步更新 `indices` 和 `prev`
4. 最后根据 `indices` 和 `prev` 回溯出具体子序列
   
function findLIS(nums) {
  if (nums.length === 0) return [];

  const n = nums.length;
  const tails = []; // 存储长度为i+1的LIS的末尾元素最小值
  const indices = []; // 存储tails中元素在nums中的索引
  const prev = new Array(n).fill(-1); // 记录每个元素的前一个元素索引

  for (let i = 0; i < n; i++) {
    const num = nums[i];
    // 二分查找tails中第一个 >= num的位置
    let left = 0, right = tails.length;
    while (left < right) {
      const mid = (left + right) >> 1;
      if (tails[mid] < num) {
        left = mid + 1;
      } else {
        right = mid;
      }
    }

    // 记录前一个元素的索引(若left>0,则前一个元素是tails[left-1]对应的索引)
    if (left > 0) {
      prev[i] = indices[left - 1];
    }

    // 更新tails和indices
    if (left === tails.length) {
      tails.push(num);
      indices.push(i);
    } else {
      tails[left] = num;
      indices[left] = i;
    }
  }

  // 回溯构建最长子序列
  const lis = [];
  let currentIndex = indices[indices.length - 1]; // 最长子序列的最后一个元素索引
  while (currentIndex !== -1) {
    lis.unshift(nums[currentIndex]);
    currentIndex = prev[currentIndex];
  }

  return lis;
}

// 示例
console.log(findLIS([10, 9, 2, 5, 3, 7, 101, 18])); // 输出:[2, 3, 7, 101] 或 [2, 5, 7, 18]

为了更直观理解 Vue 2 和 Vue 3 的 diff 算法差异,我们通过可视化流程图实例对比来描述执行流程(以同级子节点更新为例)。

场景假设

假设有一组子节点需要更新,新旧节点列表如下(key 代表节点唯一标识):

  • 旧节点:[A, B, C, D, E](key: 1,2,3,4,5)
  • 新节点:[A, C, D, F, E](key: 1,3,4,6,5)

一、Vue 2 的 diff 算法流程(双指针遍历)

核心逻辑:双指针从两端向中间收缩,优先对比首尾节点,未命中则全量查找

步骤可视化:

初始状态:  
旧节点:[A(1), B(2), C(3), D(4), E(5)]  → 指针:startIdx=0,endIdx=4  
新节点:[A(1), C(3), D(4), F(6), E(5)]  → 指针:startIdx=0,endIdx=4  

1. 对比旧头(A)和新头(A):key 相同,复用节点,双指针右移  
   旧:[A, B(2), C(3), D(4), E(5)]  → startIdx=1  
   新:[A, C(3), D(4), F(6), E(5)]  → startIdx=1  

2. 对比旧尾(E)和新尾(E):key 相同,复用节点,双指针左移  
   旧:[B(2), C(3), D(4), E]  → endIdx=3  
   新:[C(3), D(4), F(6), E]  → endIdx=3  

3. 对比旧头(B)和新尾(F):key 不同,未命中  
   对比旧尾(D)和新头(C):key 不同,未命中  

4. 全量查找:遍历旧节点剩余部分(B, C, D),用新头(C)的 key 查找  
   找到 C(旧索引=2),复用节点,将 C 移动到新头位置,新指针右移  
   旧:[B(2), C, D(4)]  → (C 被标记移动)  
   新:[C, D(4), F(6)]  → startIdx=2  

5. 对比旧头(B)和新头(D):key 不同,未命中  
   对比旧尾(D)和新头(D):key 相同,复用节点,双指针左移  
   旧:[B(2), C, D]  → endIdx=2  
   新:[D, F(6)]  → startIdx=3,endIdx=2(新指针交叉,循环结束)  

6. 清理剩余节点:  
   - 新节点剩余 [F(6)]:创建 F 插入  
   - 旧节点剩余 [B(2)]:删除 B

流程图特点:

  • 指针不断收缩,优先处理首尾匹配的节点
  • 未命中时需遍历旧节点查找,效率较低
  • 节点移动逻辑依赖指针位置调整,步骤繁琐

二、Vue 3 的 diff 算法流程(前缀/后缀 + LIS)

核心逻辑:先复用前缀/后缀相同节点,再用“最长递增子序列(LIS)”优化中间差异节点的移动

步骤可视化:

初始状态:  
旧节点:[A(1), B(2), C(3), D(4), E(5)]  
新节点:[A(1), C(3), D(4), F(6), E(5)]  

1. 复用前缀相同节点:从头部开始对比,A 的 key 相同,直接复用,剩余节点:  
   旧剩余:[B(2), C(3), D(4), E(5)]  
   新剩余:[C(3), D(4), F(6), E(5)]  

2. 复用后缀相同节点:从尾部开始对比,E 的 key 相同,直接复用,剩余节点:  
   旧剩余:[B(2), C(3), D(4)]  
   新剩余:[C(3), D(4), F(6)]  

3. 处理中间差异(新旧剩余都不为空):  
   a. 建立新节点 key 映射表(快速查找):  
      keyToNewIndexMap = { 3:0, 4:1, 6:2 }(新剩余节点的索引)  
   b. 遍历旧剩余节点,标记可复用节点在新剩余中的位置,生成 source 数组:  
      - 旧 B(2):新节点中无,标记为 -1  
      - 旧 C(3):对应新索引 0 → source[0] = 0  
      - 旧 D(4):对应新索引 1 → source[1] = 1  
      → source = [-1, 0, 1]  
   c. 计算 source 中有效索引(非 -1)的最长递增子序列(LIS):  
      有效索引为 [0, 1],LIS 为 [0, 1](长度 2)  
   d. 基于 LIS 移动节点:  
      - LIS 对应的节点(C、D)无需移动(已在正确顺序)  
      - 新节点剩余的 F(6) 是新增节点,插入到 D 之后  

4. 清理剩余节点:  
   - 旧节点中未复用的 B(2):删除

流程图特点:

  • 前缀/后缀快速复用,减少对比次数
  • 哈希表映射替代全量查找,效率提升
  • LIS 确定“无需移动的节点序列”,仅插入新增节点,移动次数最少

三、直观对比总结

阶段Vue 2 流程Vue 3 流程
初始对比双指针从两端向中间遍历先对比前缀,再对比后缀
查找复用节点遍历旧节点(O(n))哈希表映射(O(1))
节点移动逻辑依赖指针调整,移动次数多基于 LIS 确定不动节点,仅插入新增节点
最终操作需多次移动+删除+创建极少移动,主要是删除无用节点+创建新增节点

通过可视化可以看出,Vue 3 的 diff 算法通过减少对比范围、加速查找、优化移动逻辑,显著提升了性能,尤其在长列表场景下优势明显。

贡献者

The avatar of contributor named as jiechen jiechen

页面历史

撰写