Vue3 最长递增子序列详解

article/2025/9/21 0:22:17

Vue3 最长递增子序列研究

本文初衷

彻底讲清楚 Vue3 源码中实现最长递增子序列的算法。

概念名词

**最长递增子序列:**在一个给定的数值序列中,找到一个子序列,使得这个子序列元素的数值依次递增,并且这个子序列的长度尽可能地大。最长递增子序列中的元素在原序列中不一定是连续的。

比如:

序列 [10, 9, 2, 5, 3, 7, 101, 18] 的最长递增子序列是 [2, 3, 7, 101][2, 3, 7, 18]

序列 [3, 2, 8, 9, 5, 6, 7, 11, 15, 4] 的最长递增子序列是 [2, 5, 6, 7, 11, 15]

Vue3 使用最长递增子序列的背景

在 《Vue.js 设计与实现》第 11 章我们了解到 Vue3 在进行新子节点和旧子节点 DOM Diff 的方式是,先同步头部节点(处理相同的前置元素),再同步尾部节点(处理相同的后置元素),接下来判断哪些子节点需要移动,并且处理如何移动。

在处理子节点如何移动的问题上,使用了最长递增子序列。

为什么要用最长递增子序列?

(这段描述摘自:黄轶《Vue.js 3.0 核心源码解析》)

举个简单的例子具体看一下

var prev = [1, 2, 3, 4, 5, 6]var next = [1, 3, 2, 6, 4, 5]

从 prev 变成 next,数组里的一些元素的顺序发生了变化,我们可以把子节点类比为元素,现在问题就简化为我们如何用最少的移动使元素顺序从 prev 变化为 next 。

一种思路是在 next 中找到一个递增子序列,比如 [1, 3, 6] 、[1, 2, 4, 5]。之后对 next 数组进行倒序遍历,移动所有不在递增序列中的元素即可。

如果选择了 [1, 3, 6] 作为递增子序列,那么在倒序遍历的过程中,遇到 6、3、1 不动,遇到 5、4、2 移动即可,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3TXtDhk2-1648818081393)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4ba50123-4734-445e-b7b7-48bdbe112546/Untitled.png)]

如果选择了 [1, 2, 4, 5] 作为递增子序列,那么在倒序遍历的过程中,遇到 5、4、2、1 不动,遇到 6、3 移动即可,如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DQosxXrD-1648818081395)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/2ec300e0-659f-4176-bbdd-2f7cc31b5d96/Untitled.png)]

可以看到第一种移动了三次,而第二种只移动了两次,递增子序列越长,所需要移动元素的次数越少,所以如何移动的问题就回到了求解最长递增子序列的问题

实现最长递增子序列

需要明确的是,我们现在要做的是实现 Vue3 DOM Diff 中的最长递增子序列,这和力扣题库中的 300. 最长递增子序列 有些差别。力扣题求解的是最长递增子序列的长度,我们的 getRequence 函数返回值是一个下标数组。但实现方式上都是采用 贪心 + 二分查找。

给定数组 [3, 2, 8, 9, 5, 6, 7, 11, 15, 4],求解该数组的最长递增子序列?

肉眼可见,该数组最长递增子序列为 [2, 5, 6, 7, 11, 15],对应下标数组为 [ 1, 4, 5, 6, 7, 8 ]

再次提醒,我们函数返回值是:下标数组

function getRequence(arr) {}const data = [3, 2, 8, 9, 5, 6, 7, 11, 15, 4];
const res = getRequence(data);console.log('下标: ', res); // 期望输出:[ 1, 4, 5, 6, 7, 8 ] 输出的是*最长递增子序列的下标*
console.log('长度: ', res.length); // 期望输出: 6

这道题是一道中等复杂程度题目,但你不用担心,我们理清思路,其实也很简单。

1. 构建一个下标数组,保证下标对应的元素在原数组中是递增的

function getRequence(arr) {const length = arr.length;// 描述最长递增子序列的数组,元素是递增元素对应的下标const result = [0];// result 最后一个元素let resultLast;for (let i = 0; i < length; i++) {const arrI = arr[i];// 在 Vue 3 Diff 中,0 表示该新节点不在旧节点的中,是需要进行新增的节点if (arrI !== 0) {resultLast = result[result.length - 1];if (arrI > arr[resultLast]) {result.push(i);continue;}}}return result;
}const data = [3, 2, 8, 9, 5, 6, 7, 11, 15, 4];const res = getRequence(data);console.log('下标: ', res); 
console.log('长度: ', res.length);// 下标:  [ 0, 2, 3, 7, 8 ]
// 长度:  5

可以看到,首先定义了一个 result 数组,它是用于描述最长递增子序列元素在原数组对应的下标数组。然后定义了一个循环,循环中排除了元素值为 0 的情况,因为 0 在 dom diff 中是需要新增的子节点,此时我们考虑的是元素移动的情况。接下来,将当前元素与子序列最后一个元素对应的原数组的元素进行比较,如果当前元素更大,则将下标 push 进 result。

这样目前可以保证 result 数组中保存的下标是递增的,[ 0, 2, 3, 7, 8 ],但是所对应的元素值为 [3, 8, 9, 11, 15],长度为 5,很明显,2 比 3更小,可以求解更长的递增子序列 [2, 5, 6, 7, 11, 15],长度为 6

接下来,我们通过二分查找保证最长递增子序列长度正确

2. 构建一个正确长度的最长递增子序列,只需要保证数组长度正确即可

function getRequence(arr) {const length = arr.length;// 描述最长递增子序列的数组,元素是递增元素对应的下标const result = [0];// result 最后一个元素let resultLast;+  let start;
+  let end;
+  let middle;for (let i = 0; i < length; i++) {const arrI = arr[i];// 在 Vue 3 Diff 中,0 表示该新节点不在旧节点的中,是需要进行新增的节点if (arrI !== 0) {resultLast = result[result.length - 1];if (arrI > arr[resultLast]) {result.push(i);continue;}+      start = 0;
+      end = result.length - 1;
+
+      while (start < end) {
+        middle = ((start + end) / 2) | 0; // 或者 middle = Math.floor((start + end) / 2);
+
+        if (arr[result[middle]] < arrI) {
+          start = middle + 1;
+        } else {
+          end = middle;
+        }
+      }
+
+      // while 循环结束后,start 和 end 会指向同一个元素
+      if (arr[result[end]] > arrI) {
+        result[end] = i;
+      }}}return result;
}const data = [3, 2, 8, 9, 5, 6, 7, 11, 15, 4];const res = getRequence(data);console.log('下标: ', res);
console.log('长度: ', res.length);// 下标:  [ 1, 9, 5, 6, 7, 8 ],对应的arr中元素是 [2, 4, 6, 7, 11, 15]
// 长度:  6

可以看到,我们定义了 start,end 和 middle 三个指针,不断进行二分查询,中间元素 middle 小于 arrI,说明 arrI 较大,区间换成[middle + 1, end]; 否则说明,中间元素 middle 大于等于 arrI,说明 arrI 较小,区间换成 [start, middle], 如此循环,直至 start ≥ end 为止,二分找到某一项刚好大于当前项,此时 start 和 end 指针应该是指向同一个元素下标,然后用当前元素替换掉二分找到的那一项。

result 数组下标变化过程(使用 arr 数组元素描述,这样更清晰一点)// 3
// 2
// 2 8 
// 2 8 9
// 2 5 9
// 2 5 6
// 2 5 6 7
// 2 5 6 7 11
// 2 5 6 7 11 15
// 2 4 6 7 11 15

在我们的例子中,需要在数组 [2, 5, 6, 7, 11, 15] 中找到 4 应该放入的位置,需要对数组进行二分查找, start: 0, end: 6, middle: 3, 然后使用 while 不断二分查询,最终找到替换元素是 5,然后用 4替换掉 5。

很明显,4 替换 5 明显是错误的,因为最长递增子序列的顺序不能颠倒。

3. 回溯:使用前驱索引纠正最长递增子序列的偏差

回溯这个过程需要定义一个与原数组相同长度的数组 p,数组每一项保存应该排在当前元素前面元素的下标。然后经过逆序遍历数组 p,纠正 result 数组的元素。

可能文字表述有些复杂,我们画个图解释一下:

在这里插入图片描述

result 数组下表变化过程(使用 arr 数组元素描述,这样更清晰一点)// 3
// 2
// 2 8 
// 2 8 9
// 2 5 9
// 2 5 6
// 2 5 6 7
// 2 5 6 7 11
// 2 5 6 7 11 15

在数组 result 组建过程中,我们用 p 数组保存当前 result 的前一项的索引。

function getRequence(arr) {const length = arr.length;// 描述最长递增子序列的数组,元素是递增元素对应的下标const result = [0];// result 最后一个元素let resultLast;let start;let end;let middle;+  let p = arr.slice();for (let i = 0; i < length; i++) {const arrI = arr[i];// 在 Vue 3 Diff 中,0 表示该新节点不在旧节点的中,是需要进行新增的节点if (arrI !== 0) {resultLast = result[result.length - 1];if (arrI > arr[resultLast]) {result.push(i);
+        p[i] = resultLast;continue;}start = 0;end = result.length - 1;while (start < end) {middle = ((start + end) / 2) | 0; // 或者 middle = Math.floor((start + end) / 2);if (arr[result[middle]] < arrI) {start = middle + 1;} else {end = middle;}}// while 循环结束后,start 和 end 会指向同一个元素if (arr[result[end]] > arrI) {result[end] = i;
+        p[i] = result[end - 1];}}}+  let i = result.length;
+  let last = result[i - 1];
+
+  while (i-- > 0) {
+    result[i] = last;
+   last = p[last];
+  }return result;
}const data = [3, 2, 8, 9, 5, 6, 7, 11, 15, 4];const res = getRequence(data);console.log('下标: ', res);
console.log('长度: ', res.length);// 下标:  [ 1, 4, 5, 6, 7, 8 ]
// 长度:  6

定义了数组 p 长度与 arr 数组长度保持一致,p[i] 是描述应该在当前元素前面元素的下标,方便回溯时纠正第 2 步生成的有问题的 result 数组。result 数组中最后一项肯定是正确的下标位置,我们通过 p 数组不断迭代,修正 result 中存储下标错误的问题。


http://chatgpt.dhexx.cn/article/skUoujRM.shtml

相关文章

Java 最长递增子序列_最长递增子序列问题 Java

最长递增子序列问题 LIS(longest increasing subsequence) 例如 给定一个数列&#xff0c;长度为N&#xff0c; 求这个数列的最长上升(递增)子数列(LIS)的长度. 以 1, 7, 2, 8, 3, 4 为例。 这个数列的最长递增子数列是 1 2 3 4&#xff0c;长度为4&#xff1b; 次长的长度为3&…

最长递增子序列

问题 给定一个长度为N的数组,找出一个最长的单调自增子序列(不一定连续,但是顺序不能乱)。例如:给定一个长度为6的数组A{5, 6, 7, 1, 2, 8},则其最长的单调递增子序列为{5,6,7,8},长度为4. 解法1:最长公共子序列法 这个问题可以转换为最长公共子序列问题。如…

动态规划设计方法详解最长递增子序列

很多读者反应&#xff0c;就算看了前文动态规划详解&#xff0c;了解了动态规划的套路&#xff0c;也不会写状态转移方程&#xff0c;没有思路&#xff0c;怎么办&#xff1f;本文就借助「最长递增子序列」来讲一种设计动态规划的通用技巧&#xff1a;数学归纳思想。 最长递增…

最长递增子序列(Longest Increasing Subsequence)

定义 最长上升子序列&#xff08;Longest Increasing Subsequence&#xff0c;LIS&#xff09;&#xff0c;在计算机科学上是指一个序列中最长的单调递增的子序列。 问题描述 给定一个长度为 N 的数组&#xff0c;找出一个最长的单调自增子序列&#xff08;不一定连续&#…

最长递增子序列问题(你真的会了吗)

目录 一.最长递增子序列问题I 二.最长递增子序列问题II 三. 最长递增子序列问题III 一.最长递增子序列问题I 1.对应牛客网链接 最长上升子序列(一)_牛客题霸_牛客网 (nowcoder.com) 2.题目描述&#xff1a; 3.解题思路 1.首先我们分析题意&#xff1a;最长递增子序列拆&#x…

UDP协议详细解析

一、基本概念 基本定义&#xff1a;UDP 是User Datagram Protocol的简称&#xff0c; 中文名是用户数据报协议&#xff0c;是OSI&#xff08;Open System Interconnection&#xff0c;开放式系统互联&#xff09; 参考模型中一种无连接的传输层协议&#xff0c;提供面向事务的…

tcp read 和 udp recvfrom

udp中写一个长度为0的数据报是可行的&#xff0c;这导致一个包含IP头部、udp头部和没有数据的IP数据报。这也意味着对于数据报协议&#xff0c;recvfrom返回0值也是可行的&#xff1a;它不表示对方已经关闭了连接&#xff0c;这与tcp套接口上read返回0值的情况不同。由于udp是无…

UDPTCP

目录 Socket 一.UDP 特点 基于UDP实现回显服务器 服务器 客户端 端口冲突 图解 二.TCP 特点 基于TCP实现回显服务器 服务器 客户端 图解 Socket Socket套接字&#xff0c;是由系统提供用于网络通讯的技术&#xff0c;是基于TCP/IP协议的网络通信的基本操作单元&#xff0c;基于…

Reliable UDP

Reliable UDP&#xff08;可靠的UDP&#xff09;是一套服务品质的增强&#xff0c;比如拥挤控制调整&#xff0c;数据重传&#xff0c;薄化服务器算法等&#xff0c;这些增强可以提高服务器在数据包丢失和网络拥挤的条件下向RTP客户表现品质良好的RTP流的能力。Reliable UDP’的…

TCP /UDP

TCP与UDP工作在传输层&#xff0c;在程序之间传数据&#xff08;视频&#xff0c;聊天&#xff0c;图片&#xff0c;网页&#xff09; TCP基于连接的&#xff0c;可靠的&#xff08;及时知对方接受/拒绝&#xff0c;是否传错&#xff09;&#xff08;文本&#xff0c;网页&…

UDP、TCP

传输层协议UDP、TCP 一、TCP/UDP的任务二、UDP1.UDP概述2.UDP报文格式3.使用UDP的应用层协议 三、TCP1.TCP概述2.TCP报文3.TCP三次握手4.四次挥手5.超时重传6.流量控制和快重传7.拥塞控制8.延迟应答、捎带应答9.粘包问题10.基于TCP的应用层协议 四、总结 一、TCP/UDP的任务 我们…

tcp udp proxy

服务目的 首先如下图所示&#xff1a; 作为一个内外网的通信&#xff0c;必须使用tcp 和 udp 的proxy 把内网和外网打通&#xff0c;比如中间是一个有两个网卡的路由器&#xff0c;打通以后&#xff0c;由proxy 发送数据到服务端&#xff0c;服务端按照上图处于外网。 服务端…

UDP-RTP协议解析

一、RTP协议 数据传输协议RTP&#xff0c;用于实时传输数据。RTP报文由两部分组成&#xff1a;报头和有效载荷 二、RTP的会话过程 当应用程序建立一个RTP会话时&#xff0c;应用程序将确定一对目的传输地址。目的传输地址由一个网络地址和一对端口组成&#xff0c;有两个端口&a…

UDP 理解

这里需要指出的一点是&#xff0c;伪首部完全是虚拟的&#xff0c;它并不会和用户数据报一起被发送出去&#xff0c;只是在校验和的计算过程中会被使用到&#xff0c;伪首部主要来自于运载UDP报文的IP数据报首部&#xff0c;将源IP地址和目的IP地址加入到校验和的计算中可以验证…

关于TCP/UDP

目录 1、TCP协议 1.1 TCP协议格式 1.2 TCP协议原理 2、UDP协议 在学习TCP/UDP之前先来了解以下整体的通信传输&#xff0c;它是一个向下封装、向上分用的过程&#xff1a; 这是TCP/IP四层模型&#xff0c;所以要想实现通讯&#xff0c;通过TCP建立和断开连接是至关重要的&a…

UDP详解

1、UDP数据包格式 UDP 是User Datagram Protocol的简称&#xff0c; 中文名是用户数据报协议&#xff0c;是OSI&#xff08;Open System Interconnection&#xff0c;开放式系统互联&#xff09; 参考模型中一种无连接的传输层协议&#xff0c;提供面向事务的简单不可靠信息传…

TCPUDP相关介绍

TCP and UDP TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是面向连接的协议&#xff0c;也就是说&#xff0c;在收发数据前&#xff0c;必须和对方建立可靠的连接。 一个TCP连接必须要经过三次握手才能建立起来。断开连接需要四次挥手才…

TCPUDP

TCP&#xff1a;面向连接的服务&#xff0c;可靠的进程到进程的通信协议。&#xff08;因为TCP里面封装了端口号&#xff0c;端口号就意味着一个服务&#xff0c;进程&#xff09;&#xff1b;应用场景&#xff1a;如&#xff1a;文件传输&#xff1b;HTTP应用层协议 UDP&…

TCP/UDP

Tcp / ip : 应用层、传输层、网络层、网络接口层 查看本机ip&#xff1a; windons r &#xff08;进入交互换环境&#xff09;ipconfigping 本机ip 查看本机网络有无问题 端口&#xff1a; 知名端口(固定端口)&#xff1a;0—1023动态端口&#xff1a;程序可以设置的端口 1…

UDP协议的详细解析

UDP数据报 一、UDP的概述&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09; UDP是传输层的协议&#xff0c;功能即为在IP的数据报服务之上增加了最基本的服务&#xff1a;复用和分用以及差错检测。 UDP提供不可靠服务&#xff0c;具有TCP所没有的优…