← 中文资源

简体中文版本

最长递增子序列

用二分维护最小尾元素,在 O(n log n) 时间内求最长递增子序列并还原下标。

阅读英文原文

功能说明

longestIncreasingSubsequence 返回数组中最长递增子序列的长度、具体值与原始下标。strict 为真时要求严格递增,为假时允许相等元素。算法使用耐心排序与二分查找,将动态规划的 O(n²) 降为 O(n log n),适合版本序列分析、日志趋势、排期依赖和任意可比较数值序列。

关键代码

tails 保存每个长度对应的最小结尾值,tailIndices 记录该结尾在原数组中的位置,previous 为每个元素记录前驱下标。处理新值时用二分找到第一个不小于它的位置,严格模式比较小于,非严格模式比较小于等于,从而决定替换位置。更新 tails 后写入前驱,最后从最长长度的结尾回溯并反转,得到完整下标链。

使用注意

tails 数组本身不是最终子序列,只有通过 previous 回溯才能得到真实下标,直接返回 tails 是常见错误。算法对 NaN 会拒绝,因为比较不成立。非严格模式允许相等值连续出现,但返回的是其中一条合法子序列。输入不会被修改。若只需要长度,可以省略前驱数组以节省内存。

验证方式

测试空数组、单元素、严格递增、严格递减、全部相等与含负数序列。用经典序列 10,9,2,5,3,7,101,18 验证长度为四,并检查返回下标对应的值确实递增。对比暴力动态规划在随机小数组上的长度,覆盖 strict 为 false 时相等元素可连续选择的行为,并测试 NaN、Infinity 与字符串元素会抛错。

代码示例

export interface LisResult {
  length: number;
  sequence: number[];
  indices: number[];
}

export function longestIncreasingSubsequence(values: readonly number[], strict = true): LisResult {
  if (!values.every((value) => Number.isFinite(value))) {
    throw new TypeError("values must contain finite numbers");
  }
  const tails: number[] = [];
  const tailIndices: number[] = [];
  const previous = new Array<number>(values.length).fill(-1);
  for (let index = 0; index < values.length; index += 1) {
    const value = values[index]!;
    let low = 0;
    let high = tails.length;
    while (low < high) {
      const middle = (low + high) >> 1;
      const goesRight = strict ? tails[middle]! < value : tails[middle]! <= value;
      if (goesRight) low = middle + 1;
      else high = middle;
    }
    tails[low] = value;
    tailIndices[low] = index;
    previous[index] = low > 0 ? tailIndices[low - 1]! : -1;
  }
  const indices: number[] = [];
  let cursor = tailIndices.length > 0 ? tailIndices[tailIndices.length - 1]! : -1;
  while (cursor !== -1) {
    indices.push(cursor);
    cursor = previous[cursor]!;
  }
  indices.reverse();
  return {
    length: indices.length,
    sequence: indices.map((index) => values[index]!),
    indices,
  };
}