vue3 - 最长递增子序列
最近在看vue3 快速diff的源码,看到getSequence函数 处理最长递增子序列。便拿出来 研究了一番。
以下纯属 个人观点 里面可能有 瑕疵。
之所以还放出来:
- 为了让大家印证和纠正。
- 或许里面有些点能对不了解的同学有少许帮助。
喷吧~ 不怕~ 大小都接着~
多余的不说,直接上货 (源码)
// O(n log n)
function getSequence(arr) {
const p = arr.slice(); // 用于回溯, 针对 回溯 下文阐述
const len = arr.length;
const result = [0]; // 递增数组 - 存放arr 下标
let i, j, u, v, c
// 时间复杂度:O(n)
for(i = 0; i < len; i++) {
const arrI = arr[i];
if (arrI !== 0) {
j = result[result.length - 1]; // 递增数组中 最后一项, i.e. 递增数组中存在的最大值的下标
if (arr[j] < arrI) {
// 当前值 比 已产生结果最大值还 大
p[i] = j; // p 序列 的 i 下标 存放比当前 值 小一点的值对应的 下标 i.e. (push前result中的存放索引对应的最大值的索引)
result.push(i); // 将该值对应下标 放入结果
continue
// 当前 判断 处理 持续 递增情况, 若 arr 元素 规律 为 连续递增 i.e. (arr = [1,2,3,4,5,6,7,8,9])
}
// 不满足 arr[j] < arrI
// e.g.(arr = [7, 2, 3, 10, 4, 6, 5 ]; result = [1, 2, 4, 5]; arrI = 5;)
// 因此 5所对应的索引 应处于 当前result中的某个位置,通过二分查找 找到 5 应位于result中哪个位置
u = 0;
v = result.length;
// result中存放的索引所对应的值 呈现递增(值满足递增,但递增的值的顺序 不一定和 arr中值的顺序一致, 后续通过回溯解决这个问题) 因此 可以使用 二分查找 降低时间复杂度 (二分查找:O(log n))
while(u < v) {
// e.g.
// 1: u = 0, v = 4, c = 2
// resutl[c] = 4; arr[result[c]] = 4; arrI = 5;
// 4 < 5; u = c + 1 = 2 + 1 = 3;
//
// 2: u = 3, v = 4, c = 3;
// result[c] = 5; arr[result[c]] = 6; arrI = 5;
// 6 > 5;(else分支) v = 3
// u = 3; v = 3; 不满足 u < v; 跳出while
c = (u + v) >> 1; // 中值索引
// 下述中的 左侧、右侧 是 横向坐标轴概念
if (arr[result[c]] < arrI) {
// 当前值 比 中值索引对应的值 还要大, arrI 处于中值右侧
u = c + 1; // 缩小范围 继续查找
} else {
// 当前值 比 中值索引对应的值 要小或相等,arrI 处于中值左侧
v = c; // 缩小范围
}
}
// 上述二分查找后 u 的结果 为 result中的索引对应的值,比arrI大,但再所有比arrI的值中,为最小的一个
// u = 3; result[u] = 5; arr[result[u]] = 6; arrI = 5
// 将 arrI所在下标 i 替换 result 中 u 下标元素
// 因为5小于6,为保证达成 最长递增, 需要让相邻两值 间隔越小,则后续才有更大可能 达成 最长; (贪心)
// 需要注意的是 虽然 arrI = 5 比 6 要小, 但最终不一定result[u] 这个位置就是 5。
// 因为后续可能存在某个数会替代 result[u] 这个位置, 比如 当某事arrI小于5时, 届时 仍会采用二分查找 踢替换最优的值
if (arr[result[u]] > arrI) {
if (u > 0) {
// 为何限制 u > 0 ?
// 从 代码逻辑讲: 当 u = 0时, u - 1 = -1; 对数组取下标-1的元素 显然不能拿到值
// 从 实现逻辑讲: 从 result[u - 1] 取出一个值, 放入到 p 中
// p 的目的 是回溯,p中每次存放的值 都是 比当前进入result的值要小一些的值,
// 如何判断 是 要比进入result的值要小一些?
// result 是一个 递增的数组,当向result中某个索引赋值时, 该索引 - 1 位置的值 一定小于 当前索引对应的值
// 这里再次 印证了一点 就是 p中后续存放的值 都是比当前进入result中的值 要小一些的值, 至于为何要如此做, 下面阐述
p[i] = result[u - 1];
}
result[u] = i;
}
}
}
// 下面的代码逻辑 就是 回溯
// 之前提到的回溯 究竟是个什么操作, 为何要回溯,p 这个数组 存在的意义是什么?
// 首先要明确一点: p 中存放的值 为 arr 的索引, result 中存放的值 也是 arr的索引
// 并且 p中存放的索引 和 result中 存放的索引 存在一层关系
// 每一次 循环中, p中存放的索引所对应的arr中的值, 都比 本次循环中 result中存放的索引所对应的arr的值 要小一点
// (小一点的概念: [1,3,5,7,9]; 目标数: 4; 显然 1和3 比4小, 但在所有小于4的值中,3是最大的, 我们就认为 3是比4小一点的那个值, 也就是 所有小于目标值中 最大的那个值)
// 有了上面这层关系。 如果 我们锁定 result中最后一位,是否能获得一个比result中最后一位所存放的索引对应的值 小一点 的那个值 的索引
// 这个 小一点 的那个值的索引 就在我们说的 p 序列中 存放;
// 从 result 最后一位(i.e. result中最大的索引)开始, 结合 result 和 p 的存储关系, 反向查找更新 result
// 这个过程 就是 回溯
// 回溯必须要存在吗? 不回溯是否可行
// 显然是不可以的
// result中的值,是在遍历arr时将每一次遇到的值 放入到 result 中的合适位置
// 当arr = [4, 5, 1]时, 若不回溯 则 result = [2, 1] 对应arr结果为 [1, 5]
// 显然 [1, 5] 并不是 arr的 递增子序列
// 回溯时 result = [0, 1] 对应arr结果为 [4, 5]
// 实际 通过例子 可以看出, 遍历完成后 result中的值并不符合 最长递增
// 但result 中有一个值 是一定能确定的 就是 result中的最后一位
// 通过最后一位 及 p 序列之间的关系, 回溯整个result序列,使其 符合 最长递增
u = result.length;
v = result[result.length - 1];
while(u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
总结
整个函数的处理逻辑可分为以下几步:
-
开启一个遍历,依次查看每个值,针对当前值采取下列处理措施
- 当前值比之前遍历的值
大(arr[j] < arrI),则直接将当前值放入result,并将小于当前值的索引放入p序列中 - 当前值比之前遍历的值
小, 通过二分查找锁定当前值应处于之前result什么位置,找到对应位置后将该位置的前一位放入到p序列中(需满足 u > 0),并将对应位置的值替换为当前值
- 当前值比之前遍历的值
-
经过上述遍历后 得到的
result,其长度(result.length)即为最长递增子序列的长度,其中的最大值(result最后一位)即为最长递增子序列中的最大索引 -
根据确定的
最大值回溯,生成最长递增子序列
-- End --