【vue3】diff - 最长递增子序列 - 另一个打开方式

66 阅读6分钟

vue3 - 最长递增子序列

最近在看vue3 快速diff的源码,看到getSequence函数 处理最长递增子序列。便拿出来 研究了一番。 以下纯属 个人观点 里面可能有 瑕疵

之所以还放出来:

  1. 为了让大家印证和纠正。
  2. 或许里面有些点能对不了解的同学有少许帮助。
喷吧~ 不怕~ 大小都接着~
多余的不说,直接上货 (源码)

// 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;
}
总结

整个函数的处理逻辑可分为以下几步:

  1. 开启一个遍历,依次查看每个值,针对当前值采取下列处理措施

    1. 当前值比之前遍历的值 (arr[j] < arrI),则直接将当前值放入 result,并将小于当前值的索引放入p序列中
    2. 当前值比之前遍历的值, 通过 二分查找锁定当前值应处于之前result什么位置,找到对应位置后将该位置的前一位放入到p序列中(需满足 u > 0),并将对应位置的值替换为当前值
  2. 经过上述遍历后 得到的result,其长度(result.length)即为 最长递增子序列的长度,其中的最大值(result最后一位)即为最长递增子序列中的最大索引

  3. 根据确定的 最大值 回溯,生成最长递增子序列

-- End --