getSequence方法(需要的人自然知道这是个啥玩意)——帮助未来苦于理解的朋友

105 阅读3分钟

下面是vue3源码中求最长递增子序列的实现,非0基础教学解析,如果你已经思考这个算法很久,对于算法的结构已经比较清晰,思想也有一个大致的了解,但是还是没彻底弄懂,那么你看一下我的注释或许有所帮助,我想了4天彻底弄懂了这个方法的所有细节,期间也是非常煎熬,幸好悟了,这里分享一下,帮助未来认真读源码过程中遇到困难的有缘人

// 计算arr数组的最长递增子序列的下标数组,如arr: [6, 1, 4, 2, 5, 8],1458和1258都是最长递增子序列,但是算法细节原因应该会返回1258对应的下标,也就是result: [1, 3, 4, 5](了解一下计算目标即可)
// 为了方便理解算法的思想,我们不考虑返回的是子序列元素的下标还是元素本身(不重要,都代表了那个子序列就可以了),所以下面result[i]我们直接指最长递增子序列中具体的元素而非下标,如何寻找一个最长递增子序列,使用贪心法大致的思路:
// 遍历arr数组中的每一个数,如果当前这个数比数组里的最后一个数更大,那么就将这个数插入数组的最后;反之,替换掉数组中第一个大于等于这个数的元素。result数组的长度就是最长子序列的长度
// 对于这个贪心策略,考虑一种理解方式:result数组不用于记录最终的最长子序列,result[i]的含义是以result[i]为结尾的子序列长度最长为 i,另一个角度说,长度为 i 的递增序列中,result[i]为结尾的子序列就是末尾元素最小(序列增长最缓慢)的子序列。理解了这一点,正确性就显然了。
// 但是上面也说了,result数组本身记录的不是最长子序列,但是result的构造过程就是一个找到最长递增子序列的过程,所以我们需要额外去记录构造result数组的过程中,最长子序列的信息,最终把result还原成最长子序列数组
// 我们思考,当在result数组中插入一个元素时发生了什么,首先,在插入之前,result数组每一个元素都代表了arr数组中的一个元素,也就是说,即将插入的元素肯定是在result数组中元素后面的(在arr中从左到右出现的顺序),但是呢,插入一个元素的位置,可能是在result数组的任何位置(插入在最后或者更换掉前面一个元素)
// 也就是说,我们插入一个元素时,只能保证result中已经存在的元素在新插入的元素之前。然后我们利用这个分析,执行如下操作:每次result数组中加入新元素时(这里我们用的是下标,但其实就是代表了一个元素),我们就用一个map记录新加入的元素的上一个元素,即key为当前元素,value为当前元素在递增子序列中紧挨着的上一个元素
// 1. 如果是arrI(当前考察元素) > arr[j](此时递增子序列最后一个元素),即在result数组最后添加i时,此时对于arrI来说,arrI新加进来作为递增子序列的最后一个元素,自然它之前紧挨着的递增子序列的元素即位arr[j]。所以记录p[i] = j(p是一个当作map用的数组),即我们通过p[i] = j知道了子序列中i元素上一个是j
// 2. 如果arrI <= arr[j],即要把arrI更新到result数组之前的一个位置,如下代码中我们通过二分法计算出了应该更新result[u]位置的值,在更新之前,我们同样的记录这个**新元素为结尾的**递增子序列的上一个元素是谁,它的上一个元素肯定就是result[u - 1],所以执行p[i] = result[u - 1]
// 我们经过上面的操作,遍历完所有元素后,想一下p数组(map)中的每个值有什么特点,即我们总是可以通过查找p来确定某个元素的上一个元素是什么。
// 所以,我们最后来倒叙遍历一遍p数组(map),这样从最后一个元素,找到倒数第二个元素,...,最终还原出整个递增子序列,又因为我们还原的子序列时是从result中的最后一个元素开始的,也就是说是最长的那个子序列,所以我们就找到了最长递增子序列
function getSequence(arr) {
  // p实际是是一个map的功能,p[i] = j,代表arr[i]的下标在result数组中位置的前一个位置值为j(根本看不懂,没必要看懂这句话,要想理解这句代码需要宏观理解它做了什么事情)
  // 我们所构造的result数组
  const p = arr.slice()
  const result = [0] // 初始化一个值,方便算法循环,同时这个值也并非无意义,对于只有第一个元素时,最长递增子序列就是这个元素本身
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      u = 0
      v = result.length - 1
      // 寻找arrI该插入的位置,关于这里二分的分析:https://juejin.cn/post/7222549965271711801
      while (u < v) {
        c = (u + v) >> 1
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else if(arr[result[c]] > arrI){
          v = c
        } else if(arr[result[c]] === arrI) {
          v = c 
        }
      }
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}