最长递增子序列(Longest Increasing Subsequence,LIS)正悄然成为性能分水岭。它不仅是面试的高频考点,更是 Vue3 快速 Diff 算法赖以实现 O(n log n) 复杂度的关键数据结构。
一、问题抽象:定义与复杂度边界
给定序列 A = [a₀, a₁, ..., aₙ₋₁]
,求其子序列 S ⊆ A
,满足严格单调递增且长度最大化。
- 子序列不要求连续,仅保持相对顺序;
- 存在多解时,取任意一条即可;
- 朴素回溯复杂度 Θ(2ⁿ),DP 解法 Θ(n²),贪心+二分最优 Θ(n log n)。
二、经典范式:动态规划的最优子结构
设 dp[i]
表示以 aᵢ
结尾的最长递增子序列长度,状态转移方程:
css
dp[i] = 1 + max{ dp[j] | 0 ≤ j < i ∧ aⱼ < aᵢ }
配合前驱数组 prev[i]
即可在 Θ(n²) 时空中重建整条序列。该模型直观体现「最优子结构」与「无后效性」,成为算法教材的标配示例。
三、工程级优化:贪心 + 二分查找
在工业场景下,n 往往达到 10⁴ 甚至 10⁵ 量级,Θ(n²) 不再可接受。引入以下策略:
- 贪心维护
tails[k]
:长度为 k+1 的递增子序列的最小末尾元素; - 对
tails
数组执行二分查找,将插入或替换操作降至 Θ(log n); - 引入路径回溯数组
prev
,满足重建需求。
复杂度降至 Θ(n log n),内存占用 Θ(n),兼顾计算与存储效率。
四、源码级剖析:Vue3 中的实现细节
在 @vue/runtime-core
的 getSequence
函数中,LIS 被用来优化 节点移动顺序。流程如下:
- 索引映射:将新旧节点列表按照 key 建立映射,生成索引数组
idxMap
; - 序列求解:对
idxMap
调用getSequence
,得到最长递增子序列索引; - 最小移动:非 LIS 节点即为需移动的节点,DOM 操作量随 LIS 长度线性减少;
- 零开销回溯:利用
prev
数组在 O(L) 时间内重建实际 DOM 插入顺序。
该实现跳过零值索引(对应 Vue3 对 0
的特殊处理),保证算法健壮性。
js
function getSequence(arr) {
const tails = []; // tails[i] 长度为 i+1 的 LIS 的最小尾元素
const idxs = []; // idxs[i] tails[i] 在原始数组中的索引
const prev = arr.slice(); // 前驱指针,用于回溯
for (let i = 0; i < arr.length; i++) {
const val = arr[i];
if (val === 0) continue; // Vue 源码跳过 0 的特殊处理
let left = 0, right = tails.length;
while (left < right) {
const mid = (left + right) >> 1;
if (arr[tails[mid]] < val) left = mid + 1;
else right = mid;
}
if (left === tails.length) {
tails.push(i);
} else {
tails[left] = i;
}
if (left > 0) prev[i] = tails[left - 1];
idxs[left] = i;
}
// 回溯索引
let u = tails.length, v = tails[u - 1];
while (u--) {
tails[u] = v;
v = prev[v];
}
return tails; // 返回的是索引数组
}
结论
最长递增子序列从经典算法问题跃迁为前端运行时性能基石,其 Θ(n log n) 实现已在 Vue3 中经千万级节点验证。