Vue3快速Diff算法中的最长递增子序列算法详解

99 阅读4分钟

引言

提到最长递增子序列,不免想到力扣相关的一道题:300. 最长递增子序列。在该题中,求的是递增子序列的长度,而在diff算法中,需要得到由递增子序列索引所组成的数组。
以输入: nums = [10,9,2,5,3,7,101,18]为例,在力扣中,返回长度4即可,而在diff算法中需要返回最长递增子序列的索引组成的编号:[2,4,5,7]
在Vue3中,使用贪心+二分指针+回溯来得到最长递增子序列,下面分块来讲解算法实现的关键步骤、具体例子、具体代码实现。

贪心与二分指针

总体思路

建立一个栈,栈里理应是单调递增的,但有略微的不同。考虑以下三种情况:

1.如果当前的元素大于栈的最后一个元素,或者是首个元素,那么该元素进栈
2.如果当前的元素小于栈的最后一个元素,那么寻找栈内比它稍大的元素,并替换;
- 这里的逻辑是用当前元素占了个位置,代表的还是之前的元素。
- 寻找栈内比它稍大的元素,并替换,该步骤用普通的二分法来实现
3.如果相等,那么跳过

可以看看这个帖子,理解这里的步骤2的精髓:

toutiao.io/posts/9vvtg…

举个例子

用数组[2,3,1,5,6,8,7,9,4]来讲解上述过程
先创建一个tails数组,用来存储最长递增子序列组成的索引数组。接着开始遍历输入数组nums的每一个元素。
1.tails里没有元素,所以索引0进栈。 step1.png 2.指针往后走,因为3>2,所以索引1进栈。 image.png 3.指针往后走,因为1<tails栈顶的元素,所以要替换元素。因为2比1稍大,所以tails数组中2将会替换0. image.png 4.指针继续往后走,由于后面的5,6,8都大于tails栈顶的元素,所以这三个对应的索引都进栈。 image.png 5.指针走到了元素7这里,因为7<tails栈顶的元素,所以要替换元素。因为8比7稍大,所以tails数组中6将会替换5. image.png 6.指针继续往后走,最后我们可以得到这样的tails,而索引对应的值如左边所示: image.png 最终得到的tails数组长度为6,代表最长递增子序列的长度为6.

代码实现

const lengthOfLIS = function(nums) {
    let tails = [] //存储最长递增子序列的索引
    for (let k of nums.keys()) {
        let num = nums[k]
        // 情况1: tails里没有元素或者比tails顶上的元素大,进栈
        if (tails.length === 0 || num > nums[tails[tails.length - 1]]) {     
            tails.push(k)
        }
        // 情况2: 和tails里栈顶的元素相同,跳过
        else if (num === nums[tails[tails.length - 1]]) {
            continue;
        }
        // 情况3: 小于tails里栈顶的元素,找到稍大的元素,替换
        else {
            let i = 0,j = tails.length;
            while (i < j) {
                let mid = parseInt((i+j)/2);
                if (nums[tails[mid]] < num) i=mid+1;
                else j=mid;
            } 
            tails[j] = k;
        }
    }
    console.log(tails)
    return tails.length
};

我们希望的最长递增序列为[2,3,5,6,7,9],但是根据tails里存储的索引,我们得到的最长递增序列是[1,3,4,6,7,9],并不正确。这是由于前面的替换操作引起的,下面使用回溯来修正。

回溯

代码实现

const lengthOfLIS2 = function(nums) {
    let tails = []
    // 新增p数组,key为当前元素在nums中的下标。value代表在tails栈中,上一个元素在nums中的下标
    // 形成了一个树的结构,对于p[k]= v,代表的含义是:k的父节点为v 
    let p = new Array(nums.length).fill(undefined)
    for (let k of nums.keys()) {
        let num = nums[k]
        if (tails.length === 0 || num > nums[tails[tails.length - 1]]) {
            // 情况1: tails栈中新增元素,更新父节点的引用情况
            p[k] = tails[tails.length - 1]           
            tails.push(k)
        }
        else if (num === nums[tails[tails.length - 1]]) {
            continue;
        }
        else {
            
            let i = 0,j = tails.length;
            while (i < j) {
                let mid = parseInt((i+j)/2);
                if (nums[tails[mid]] < num) i=mid+1;
                else j=mid;
            } 
            tails[j] = k;
            // 情况2: tails栈中替换元素,更新父节点的引用情况
            p[k] = tails[j-1]; 
        }
    }
    // 回溯操作
    u = tails.length
    v = tails[u - 1] // v为最深层的最右边的元素
    while (u-- > 0) {
        // 链表需要.next来找下一个节点,而我们建立的是可以往回退的树。用p[v]来往上搜索,收集从根部最右往上的一个子树。
        tails[u] = v
        v = p[v] 
    }
    console.log(tails,p)
    return tails.length
};

整体思路

  1. 新增p数组,key为当前元素在nums中的下标;value代表在tails栈中,上一个元素在nums中的下标。p形成了一个树的结构,对于p[k]= v,代表的含义是:k的父节点为v。对数组p的收集有两处:
  • tails栈顶新增元素
  • tails中替换元素
  1. 根据数组p,使用回溯收集从根部最右往上的一个子树。

举个例子

下面还是以数组[2,3,1,5,6,8,7,9,4]为例,讲解p数组的更新过程。

收集p数组

1.指针指向nums的第一个元素,此时需要新增,看看tails里0压着谁,更新p[0]=undefined. image.png 2.指针往后走,由于3>2所以在tails栈内新增元素,发现tails里1压着0,更新p[1]=0,代表索引为1的元素的父节点为索引为0的元素。 image.png 3.指针往后,由于1<栈顶的元素3,所以要替换,更新p[2]=undefined,代表着索引为2的节点已经是tails的栈底元素。 image.png 4.指针往后,由于元素5大于栈顶的元素3,所以新增,更新p[3]=1. image.png 5.指针往后走,遇到元素6,8和上面的步骤一致,然后更新tails数组和p数组 image.png 6.指针指向元素7,发现要替换,于是替换tails,看到tails数组中6压着4,代表6的父节点为4,所以p[6]=4 image.png 7.指针指向元素9,新添元素;指针指向元素4,发现要tails里存储的索引3要替换为8。由于tails里8压着1,所以p[8]=1。由此收集树的工作做好了。 image.png

构建一棵树

翻译一下p数组,对于p[k]=v,含义是p的父节点为k,也可说是v指向了k
1.p[0]=undefined,代表着0的父节点是undefined,于是我们可以画出: image.png 2.p[1]=0,代表0指向1 image.png 3.指针往后走,步骤是一样的,省略相似步骤,可以得到 image.png

回溯修正

我们要得到的递增子序列,实际上就是最深层的最右边的元素往上的这颗子树,如蓝色所示。 image.png 最终选择哪些节点,是由tails最后一个元素决定的。tails最后一个元素是7,而它记录的父节点是6;通过p[v]可以不断找到父节点,由此进行修正。 修正完毕后,输出的最长递增子序列编号为: [0, 1, 3, 4, 6, 7],确实是正确的结果。

扩展

上述修正的过程,将数组转化为了树型结构。力口:287-寻找重复数,将数组转化为了链表,通过链表是否有环来判断数组是否有重复元素。