一文揭秘Vue3中你不知道的最长递增子序列(逐行分析)

15,086 阅读3分钟

求解最长递增子序列

  • 求解最长递增子序列可使用动态规划其时间复杂度为O(N^2),Vue中另辟蹊径使用贪心+二分的方式进行求解时间复杂度为O(N * logN)

    • 具体思路为:维护一个数组result想办法让该数组增长的更加缓慢,即可求出最长递增子序列的长度,即当要查找的当前值大于数组中最大的一个值时候直接push到result数组中,当小于的时候使用二分法查找到第一个大于当前值的那个位置,然后使用当前值替换掉它,最后求得的result数组的长度就是最长递增子序列的长度。
    • 但是该方法有个问题,子序列长度是准确的其中的值却不是准确的,此时在Vue中使用了另一个数组p来缓存比当前位置小的那个位置的索引,如p[i]存的永远是比arr[i]小的那个值所在的位置,因此在下方代码中可以看到两个对p的修改的位置:
      • 位置1
      if (arr[j] < arrI) { 
          p[i] = j  
          result.push(i) 
          continue 
        }
      
      • 位置2
      if (arrI < arr[result[u]]) {  
          if (u > 0) { 
            p[i] = result[u - 1]  
          }
          result[u] = i     
      }
          ```
      
    • 第一个位置p[i]存的是j,因为上述条件判断arr[j] < arr[i],所以下方的result需要push,因此j就是小于当前值的索引位置
    • 第二个位置p[i] 存的是 result[u - 1],因为查找到的u是第一个大于等于当前值的位置,然后将当前位置重写了result[u],因此当前值的位置变成了u,那么u-1必定是小于当前值的索引位置
  • Vue3最长递增子序列代码逐行解读

function getSequence(arr: number[]) {
  const p = arr.slice() // 复制一份数组
  const result = [0] // 存放的是arr数组的索引,result数组将要存放的是递增的索引
  let i, j, u, v, c  // 声明一些变量
  const len = arr.length // 获取当前数组的长度
  for (i = 0; i < len; i++) {
    const arrI = arr[i] // 当前要比较的值为arrI
    if (arrI !== 0) { // 如果当前要比较的值不为0
      j = result[result.length - 1]   // result数组中最大的值的索引
      if (arr[j] < arrI) { // 如果当前值大于result中的最大值,那么就将当前值添加到result数组中,意思是递增序列长度增加1
        p[i] = j   // 因为result在这里马上会push进去一个值,当前位置存储的是还没push前的最后一位也就是push后的前一位的索引
        result.push(i) // 也就是当前值arrI 所在位置的前一位的索引
        continue // 这里直接退出当前循环 下方就不需要使用else块
      }
      
      // 开始二分
      u = 0    //左指针初始值为0
      v = result.length - 1 // 右指针初始值为数组长度-1,也就是最大索引
      while (u < v) {  // 当左指针小于右指针时,才需要进入循环
        c = (u + v) >> 1  // 这个位置是取中间值,Vue最初的代码是 ((u + v)/ 2) | 0 后来改成了 (u+v)>>1, 更好的方式是 u+ ((v-u) >> 1) 可以避免指针越界,不过在vue中节点的数量远达不到越界的情况可暂时忽略
        if (arr[result[c]] < arrI) { // 如果中间值的位置的值小于当前值
          u = c + 1   // 那么就说明要找的值在中间值的右侧,因此左指针变为中间值+1
        } else {      // 否则就是大于等于当前值
          v = c       // 那么右指针变为中间值,再进行下一次循环
        }
      }   // 最后输出的左指针的索引一定是非小于当前值的,有可能大于也有可能等于
      if (arrI < arr[result[u]]) {  // 如果当前值小于第一个非小于的值,那么就意味着这个值是大于的,排除了等于的情况。
        if (u > 0) {  // 如果u === 0 说明当前值是最小的,不会有比它小的值,那么它前面不会有任何的值,只有u大于0时才需要存储它前面的值
          p[i] = result[u - 1]  //  当前位置因为result[u]马上就被arrI替换,所以result[u - 1]就是当前值存储位置的前一位,也就是比当前值小的那个值所在的位置
        }
        result[u] = i     // 将第一个比当前值大的值替换为当前值,依次来让数组递增的更缓慢
      }
    }
  }
  // 使用二分可以找到最长的长度但是无法判断最长的序列
  // 开始回溯倒序找到最长的序列,因为p中当前位置存放的是上一个比当前值小的数所在的位置,所以使用倒序
  u = result.length // 获取递增数组的长度
  v = result[u - 1]  // 获取递增数组的最后一项也就是最大值的索引
  while (u-- > 0) {  // 当u的索引没有越界时一直循环
    result[u] = v   // 一开始result的最后一个值存放的索引一定是最大值
    v = p[v]   // 根据当前值就是该序列中最大的部分来查找是谁跳动这个位置的,依次往前查
  }
  return result  // 最后输出结果数组,此数组中存放的就是最大递增子序列的索引值
  • 最后的回溯分析: image.png

    image.png