最长递增子序列算法——逐行分析、看完就懂

61 阅读3分钟

在 Vue3 Diff 算法中,有一个求解最长递增子序列长度的函数:源码链接,我想在这里以一种比较浅显易懂的方法介绍下这个算法。

  • 输入:一个长度不为 0 的数组,每个索引对应一个大于等于 0 的整数
  • 输出:返回其中最长严格递增子序列对应的索引列表(0 可以不计入最长递增子序列中)
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n)
/**
 * @param {number[]} arr
 * @return {number[]}
 */
function getSequence(arr) {
  // 新建一个数组,保存当某个索引的值为子序列最大值时前面一个值的索引
  const preIndexMap = arr.slice()
  // 递增子序列,默认填充第一个元素的索引
  const result = [0]
  // 从第二个索引开始遍历
  for (let i = 1; i < arr.length; i++) {
    const num = arr[i]
    // 忽略 0,因为在 vue 中这里的 0 是个特殊值,代表对应新节点在旧节点列表中没有匹配节点,即新增节点
    if (num === 0) continue

    const maxIndex = result[result.length - 1]
    if (num > arr[maxIndex]) {
      // 比当前子序列最大值还大,扩充子序列
      result.push(i)
      // 记录索引前面的索引
      preIndexMap[i] = maxIndex
    } else {
      // 要让序列上升的更慢,才能找到更长的递增子序列
      let l = 0 // 目标是找到刚好比 num 大的值,并替换它
      let r = result.length - 1
      while (l < r) {
        let m = l + ((r - l) >> 1)
        // 因为实际输入不太可能出现相等的情况,所以没有单独做相等判断
        // 否则我们可以在相等时令 l = m 提前退出循环提升算法效率
        if (arr[result[m]] < num) {
          // 小于 num,则肯定不能放这里,往右边继续寻找位置
          l = m + 1
        } else {
          // 大于 num,可能为解,不过左边可能还有更合适的位置,继续循环
          r = m
        }
      }

      // 相等情况可以跳过(说明用户使用了相同的 key)
      if (arr[result[l]] === num) continue
      // num 可以放在 l 这个位置让子序列上升得更慢
      result[l] = i
      // 记录索引前面的索引
      if (l > 0) {
        preIndexMap[i] = result[l - 1]
      }
    }
  }

  let n = result.length
  let index = result[n - 1]
  while (n-- > 0) {
    result[n] = index
    index = preIndexMap[index]
  }

  return result
}

这个算法主要是贪心策略比较难理解:要让序列上升的更慢,才能找到更长的递增子序列。我们的 result 数组最开始只是作为辅助数组,每一项 result[i] 的含义是,所有长度为 i+1 的递增子序列的末尾元素中的最小值的索引。

  • 假设当前辅助数组保存的递增子序列为 [2, 5]
    • 如果新的 num 值为 8,那么辅助数组应该为 [2, 5, 8],这个很好理解
    • 如果新的 num 值为 3,那么应该替换 5 所在的位置 [2, 3],这样子序列就会上升的更慢
      • 如果下一个 num 值为 4,就能构成更长的子序列 [2, 3, 4]

但是辅助数组保存的不是真实的最长递增子序列,比如对于数组 [2, 5, 8, 3],辅助数组保存的是 [2, 3, 8],而实际递增子序列为 [2, 5, 8]

  • 所以我们使用了一个数组 preIndexMap 来保存当某个索引的值为子序列最大值时前面一个值的索引,每次更新辅助数组 result 时,同时更新 preIndexMap 来记录真实的子序列顺序。
    • 我们更新 5 的时候,记录了 5 前面是 2
    • 我们更新 8 的时候,记录了 8 前面是 5
    • 这样当我们发现最后辅助数组最后一个元素是 8 时,就可以通过 preIndexMap 找到实际的递增子序列为 [2, 5, 8]