当前位置: 首页 > news >正文

vue diff 前后缀+最长递增子序列算法

文章目录

  • 查找相同前后缀
    • 通过前后缀位置信息新增节点
    • 通过前后缀位置信息删除节点
  • 中间部份 diff
    • 判断节点是否需要移动
    • 删除节点
      • 删除未查找到的节点
      • 删除多余节点
    • 移动和新增节点
      • 最长递增子序列
    • 求解最长递增子序列位置信息

查找相同前后缀

在这里插入图片描述

  • 如上图所示,新旧 children 拥有相同的前缀节点和后缀节点
  • 对于前缀节点,我们可以建立一个索引,指向新旧 children 中的第一个节点,并逐步向后遍历,直到遇到两个拥有不同 key 值的节点为止
// 更新相同的前缀节点
// j 为指向新旧 children 中第一个节点的索引
let j = 0
let prevVNode = prevChildren[j]
let nextVNode = nextChildren[j]
// while 循环向后遍历,直到遇到拥有不同 key 值的节点为止
while (prevVNode.key === nextVNode.key) {// 调用 patch 函数更新patch(prevVNode, nextVNode, container)j++prevVNode = prevChildren[j]nextVNode = nextChildren[j]
}
  • 对于相同的后缀节点,由于新旧 children 中节点的数量可能不同,所以我们需要两个索引分别指向新旧 children 的最后一个节点,并逐步向前遍历,直到遇到两个拥有不同 key 值的节点为止
// 更新相同的后缀节点// 指向旧 children 最后一个节点的索引
let prevEnd = prevChildren.length - 1
// 指向新 children 最后一个节点的索引
let nextEnd = nextChildren.length - 1prevVNode = prevChildren[prevEnd]
nextVNode = nextChildren[nextEnd]// while 循环向前遍历,直到遇到拥有不同 key 值的节点为止
while (prevVNode.key === nextVNode.key) {// 调用 patch 函数更新patch(prevVNode, nextVNode, container)prevEnd--nextEnd--prevVNode = prevChildren[prevEnd]nextVNode = nextChildren[nextEnd]
}

通过前后缀位置信息新增节点

// 前缀节点终止位置
j: 1
// 后缀节点终止位置
prevEnd: 0
nextEnd: 1
  • 发现 j > prevEnd 并且 j <= nextEnd,这说明当新旧 children 中相同的前缀和后缀被更新之后,旧 children 中的节点已经被更新完毕了,而新 children 中仍然有剩余节点,通过上图可以发现,新 children 中的 li-d 节点,就是这个剩余的节点。实际上新 children 中位于 j 到 nextEnd 之间的所有节点都应该是新插入的节点:
    在这里插入图片描述
// 满足条件,则说明从 j -> nextEnd 之间的节点应作为新节点插入
if (j > prevEnd && j <= nextEnd) {// 所有新节点应该插入到位于 nextPos 位置的节点的前面const nextPos = nextEnd + 1const refNode =nextPos < nextChildren.length ? nextChildren[nextPos].el : null// 采用 while 循环,调用 mount 函数挂载节点while (j <= nextEnd) {mount(nextChildren[j++], container, false, refNode)}
}

通过前后缀位置信息删除节点

在这里插入图片描述

  • 在这个案例中,当“去掉”相同的前缀和后缀之后,三个索引的值为:
j: 1
prevEnd: 1
nextEnd: 0
  • 这时条件 j > nextEnd 并且 j <= prevEnd 成立,通过上图可以很容的发现,旧 children 中的 li-b 节点应该被移除,实际上更加通用的规则应该是:在旧 children 中有位于索引 j 到 prevEnd 之间的节点,都应该被移除
if (j > prevEnd && j <= nextEnd) {// j -> nextEnd 之间的节点应该被添加const nextPos = nextEnd + 1const refNode =nextPos < nextChildren.length ? nextChildren[nextPos].el : nullwhile (j <= nextEnd) {mount(nextChildren[j++], container, false, refNode)}
} else if (j > nextEnd) {// j -> prevEnd 之间的节点应该被移除while (j <= prevEnd) {container.removeChild(prevChildren[j++].el)}
}
  • 假设在第一个 while 循环结束之后,索引 j 的值已经大于 prevEnd 或 nextEnd,那么还有必须执行第二个 while 循环吗?答案是没有必要,这是因为一旦索引 j 大于 prevEnd 则说明旧 children 中的所有节点都已经参与了 patch,类似的,如果索引 j 大于 nextEnd 则说明新 children 中的所有节点都已经参与了 patch,这时当然没有必要再执行后续的操作了。
while (prevVNode.key === nextVNode.key) {patch(prevVNode, nextVNode, container)j++if (j > prevEnd || j > nextEnd) {break;}prevVNode = prevChildren[j]nextVNode = nextChildren[j]
}
// 更新相同的后缀节点
prevVNode = prevChildren[prevEnd]
nextVNode = nextChildren[nextEnd]
while (prevVNode.key === nextVNode.key) {patch(prevVNode, nextVNode, container)prevEnd--nextEnd--if (j > prevEnd || j > nextEnd) {break outer}prevVNode = prevChildren[prevEnd]nextVNode = nextChildren[nextEnd]
}if(!(j > prevEnd && j>prevEnd)){// 满足条件,则说明从 j -> nextEnd 之间的节点应作为新节点插入if (j > prevEnd && j <= nextEnd) {// j -> nextEnd 之间的节点应该被添加// 省略...} else if (j > nextEnd) {// j -> prevEnd 之间的节点应该被移除// 省略...}
}

中间部份 diff

在这里插入图片描述

  • 首先,我们需要构造一个数组 source,该数组的长度等于新 children 在经过预处理之后剩余未处理节点的数量,初始化该数组中每个元素的初始值为 -1
  • 实际上 source 数组将用来存储新 children 中的节点在旧 children 中的位置,后面将会使用它计算出一个最长递增子序列,并用于 DOM 移动
    在这里插入图片描述
  • 再建立一个 Index Map 中的键是节点的 key,值是节点在新 children 中的位置索引,用空间来换取时间上的优化
    在这里插入图片描述
if (j > prevEnd && j <= nextEnd) {// 省略...
} else if (j > nextEnd) {// 省略...
} else {// 构造 source 数组const nextLeft = nextEnd - j + 1  // 新 children 中剩余未处理节点的数量const source = []for (let i = 0; i < nextLeft; i++) {source.push(-1)}const prevStart = jconst nextStart = jlet moved = falselet pos = 0// 构建索引表const keyIndex = {}for(let i = nextStart; i <= nextEnd; i++) {keyIndex[nextChildren[i].key] = i}// 遍历旧 children 的剩余未处理节点for(let i = prevStart; i <= prevEnd; i++) {prevVNode = prevChildren[i]// 通过索引表快速找到新 children 中具有相同 key 的节点的位置const k = keyIndex[prevVNode.key]if (typeof k !== 'undefined') {nextVNode = nextChildren[k]// patch 更新patch(prevVNode, nextVNode, container)// 更新 source 数组source[k - nextStart] = i// 判断是否需要移动if (k < pos) {moved = true} else {pos = k}} else {// 没找到}}}

判断节点是否需要移动

  • 在上一步代码中,遍历旧 children 的剩余未处理节点,通过索引表快速找到新 children 中具有相同 key 的节点的位置
  • 如果新旧节点位置没有变化,那么位置信息应该是相同的,否则,新节点索引表信息为[1,2,3,4],如果映射成旧节点为[1,2,4,3],说明旧节点的最后一个位置和前面的位置互换了,说明节点移动了
const prevStart = j
const nextStart = j
let moved = false
let pos = 0
// 构建索引表
const keyIndex = {}
for(let i = nextStart; i <= nextEnd; i++) {keyIndex[nextChildren[i].key] = i
}
// 遍历旧 children 的剩余未处理节点
for(let i = prevStart; i <= prevEnd; i++) {prevVNode = prevChildren[i]// 通过索引表快速找到新 children 中具有相同 key 的节点的位置const k = keyIndex[prevVNode.key]if (typeof k !== 'undefined') {nextVNode = nextChildren[k]// patch 更新patch(prevVNode, nextVNode, container)// 更新 source 数组source[k - nextStart] = i// 判断是否需要移动if (k < pos) {moved = true} else {pos = k}} else {// 没找到}
}

删除节点

删除未查找到的节点

  • 拿旧 children 中的节点尝试去新 children 中寻找具有相同 key 值的节点,但并非总是能够找得到,当 k === ‘undefined’ 时,说明该节点在新 children 中已经不存在了,这时我们应该将其移除
// 遍历旧 children 的剩余未处理节点
for(let i = prevStart; i <= prevEnd; i++) {prevVNode = prevChildren[i]// 通过索引表快速找到新 children 中具有相同 key 的节点的位置const k = keyIndex[prevVNode.key]if (typeof k !== 'undefined') {// 省略...} else {// 没找到,说明旧节点在新 children 中已经不存在了,应该移除container.removeChild(prevVNode.el)}
}

删除多余节点

  • 旧 children 中已经更新过的节点数量应该小于新 children 中需要更新的节点数量,一旦更新过的节点数量超过了新 children 中需要更新的节点数量,则说明该节点是多余的节点,我们也应该将其移除
const nextLeft = nextEnd - j + 1  // 新 children 中剩余未处理节点的数量
let patched = 0
// 遍历旧 children 的剩余未处理节点
for (let i = prevStart; i <= prevEnd; i++) {prevVNode = prevChildren[i]if (patched < nextLeft) {// 通过索引表快速找到新 children 中具有相同 key 的节点的位置const k = keyIndex[prevVNode.key]if (typeof k !== 'undefined') {nextVNode = nextChildren[k]// patch 更新patch(prevVNode, nextVNode, container)patched++// 更新 source 数组source[k - nextStart] = i// 判断是否需要移动if (k < pos) {moved = true} else {pos = k}} else {// 没找到,说明旧节点在新 children 中已经不存在了,应该移除container.removeChild(prevVNode.el)}} else {// 多余的节点,应该移除container.removeChild(prevVNode.el)}
}

移动和新增节点

在这里插入图片描述

最长递增子序列

  • source 数组的值为 [2, 3, 1, -1],很显然最长递增子序列应该是 [ 2, 3 ],换算成位置信息是 [ 0, 1 ]
  • 而最长递增子序列是 [ 0, 1 ] 这告诉我们:新 children 的剩余未处理节点中,位于位置 0 和位置 1 的节点的先后关系与他们在旧 children 中的先后关系相同。或者我们可以理解为位于位置 0 和位置 1 的节点是不需要被移动的节点
  • 只有 li-b 节点和 li-g 节点是可能被移动的节点,但是我们发现与 li-g 节点位置对应的 source 数组元素的值为 -1,这说明 li-g 节点应该作为全新的节点被挂载,所以只有 li-b 节点需要被移动
  • 使用 for 循环从后向前遍历新 children 中剩余未处理的子节点
  • 这里的技巧在于 i 的值的范围是 0 到 nextLeft - 1,这实际上就等价于我们对剩余节点进行了重新编号。接着判断当前节点的位置索引值 i 是否与子序列中位于 j 位置的值相等,如果不相等,则说明该节点需要被移动;如果相等则说明该节点不需要被移动,并且会让 j 指向下一个位置
  • 节点需要被怎么移动呢?找到 li-b 节点的后一个节点(li-g),将其插入到 li-g 节点的前面即可
if (moved || source.indexOf(-1)!==-1) {// 根据 source 数组计算一个最长递增子序列:const seq = lis(sources) // [ 0, 1 ],代表的是最长递增子序列中的各个元素在 source 数组中的位置索引let j = seq.length - 1// 从后向前遍历新 children 中的剩余未处理节点for (let i = nextLeft - 1; i >= 0; i--) {if (source[i] === -1) {// 作为全新的节点挂载// 该节点在新 children 中的真实位置索引const pos = i + nextStartconst nextVNode = nextChildren[pos]// 该节点下一个节点的位置索引const nextPos = pos + 1// 挂载mount(nextVNode,container,false,nextPos < nextChildren.length? nextChildren[nextPos].el: null)} else if (i !== seq[j]) {// 说明该节点需要移动// 该节点在新 children 中的真实位置索引const pos = i + nextStartconst nextVNode = nextChildren[pos]// 该节点下一个节点的位置索引const nextPos = pos + 1// 移动container.insertBefore(nextVNode.el,nextPos < nextChildren.length? nextChildren[nextPos].el: null)} else {// 当 i === seq[j] 时,说明该位置的节点不需要移动// 并让 j 指向下一个位置j--}}
}}

求解最长递增子序列位置信息

[ 0, 8, 4, 12, 2, 10]
// 最长递增子序列长度
[ 3, 2, 2, 1, 2, 1]
  • 如上可知最长子序列长度为 3,从右往左遍历查找子序列长度为2位置,接着查找为1的位置,这样就能找到所有的位置信息
// 所有的最长递增子序列
[ 0, 8, 12 ]
[ 0, 8, 10 ]
[ 0, 4, 12 ]
[ 0, 4, 10 ]
[ 0, 2, 10 ]

相关文章:

vue diff 前后缀+最长递增子序列算法

文章目录 查找相同前后缀通过前后缀位置信息新增节点通过前后缀位置信息删除节点 中间部份 diff判断节点是否需要移动删除节点删除未查找到的节点删除多余节点 移动和新增节点最长递增子序列 求解最长递增子序列位置信息 查找相同前后缀 如上图所示&#xff0c;新旧 children 拥…...

【Python】Locust持续优化:InfluxDB与Grafana实现数据持久化与可视化分析

目录 前言 influxDB 安装运行InfluxDB 用Python 上报数据到influxdb ocust 数据写入到 influx Locust的生命周期 上报数据 优化升级 配置Grafana 总结 资料获取方法 前言 在进行性能测试时&#xff0c;我们需要对测试结果进行监控和分析&#xff0c;以便于及时发现问…...

数组模拟循环链表

5073. 空闲块 - AcWing题库 数组模拟循环链表 /*从当前位置开始遍历空闲块链表&#xff08;初始是从地址最小的第一个空闲块开始&#xff09;&#xff0c;寻找满足条件的最小块 &#xff08;即&#xff1a;大于等于请求空间的最小空闲块&#xff0c;如果有多个大小相同的最小空…...

第三章 图论 No.5最小生成树之虚拟源点,完全图与次小生成树

文章目录 虚拟源点&#xff1a;1146. 新的开始贪心或kruskal性质&#xff1a;1145. 北极通讯网络最小生成树与完全图&#xff1a;346. 走廊泼水节次小生成树&#xff1a;1148. 秘密的牛奶运输 虚拟源点&#xff1a;1146. 新的开始 1146. 新的开始 - AcWing题库 与一般的最小…...

RESTful API的讲解以及用PHP实现RESTful API

RESTful API是什么 RESTful是一种设计风格&#xff0c;是一种用于构建Web服务的架构。RESTful API是一种基于REST&#xff08;Representational State Transfer&#xff09;架构风格的Web服务接口设计规范。它强调使用HTTP协议中的请求方法&#xff08;例如GET、POST、PUT、DEL…...

Spring中@Component和@Bean的区别

1. 用途不同 Component用于标识普通类 Bean是在配置类中声明和配置Bean对象 2. 使用方式不同 Component是一个类级别的注解,Spring通过ComponentScan注解扫描并注册为Bean. Bean是一个方法级别的注解,在配置类中手动声明和配置Bean 3. 控制权不同 Component注解修饰的类使…...

【问题解决】mysql 数据库字符串分割之后多行输出方法

背景&#xff1a; 项目需要从一张表查询出来数据插入到另一张表&#xff0c;其中有一个字段是用逗号分隔的字符串&#xff0c;需要多行输入到另一张表&#xff0c;那么这个如何实现呢 方案&#xff1a; 下面先粘贴下sql语句&#xff1a; select SUBSTRING_INDEX(SUBSTRING_…...

flutter开发实战-时间显示刚刚几分钟前几小时前

flutter开发实战-时间显示刚刚几分钟前几小时前 在开发中经常遇到从服务端获取的时间戳&#xff0c;需要转换显示刚刚、几分钟前、几小时前、几天前、年月日等格式。 一、代码实现 static String timeFormatterChatTimeStamp(int seconds) {try {int nowDateSeconds (DateTi…...

导出LLaMA等LLM模型为onnx

通过onnx模型可以在支持onnx推理的推理引擎上进行推理&#xff0c;从而可以将LLM部署在更加广泛的平台上面。此外还可以具有避免pytorch依赖&#xff0c;获得更好的性能等优势。 这篇博客&#xff08;大模型LLaMa及周边项目&#xff08;二&#xff09; - 知乎&#xff09;进行…...

回顾 OWASP 机器学习十大风险

日复一日&#xff0c;越来越多的机器学习 (ML) 模型正在开发中。机器学习模型用于查找训练数据中的模式&#xff0c;可以产生令人印象深刻的检测和分类能力。机器学习已经为人工智能的许多领域提供了动力&#xff0c;包括情感分析、图像分类、面部检测、威胁情报等。 数十亿美…...

ENSP软件的基本使用命令(第三十一课)

ENSP软件的基本使用命令(第三十一课) 下面的图片是今天操作的核心基础操作 1 命令行页面 交换机 路由器 PC机 分别展示一下 页面的样子 2 基本命令结构...

五、FreeRTOS数据类型和编程规范

1、数据类型 (1)每个移植的版本都含有自己的portmacro.h头文件&#xff0c;里面定义了2个数据类型。 (2)TickType_t FreeRTOS配置了一个周期性的时钟中断&#xff1a;Tick Interrup每发生一次中断&#xff0c;中断次数累加&#xff0c;这被称为tick counttick count这个变量…...

码出高效_第二章 | 面向对象_上

目录 一. OOP理念1. 概念辨析2. 四大特性1. 抽象2. 封装3. 继承4. 多态 二. 初识Java1. JDKJDK 5-11的重要类、特性及重大改变 2. JRE关于JVM 三. 类1. 概述2. 接口和抽象类1. 概念及相同点2. 不同点3. 总结 3. 内部类4. 访问权限控制1. 由来2. public/private/无/private3. 推…...

大学生课设实训|基于springboot的在线拍卖系统

目录 项目描述 主要技术栈 功能效果 数据库设计 开发顺序 业务功能 大家好&#xff01;我是龍弟-idea&#xff01;需要源码资料信息可私聊我【HWL__666666】&#xff01; 项目描述 本系统是一个网上商品竞拍系统&#xff0c;为拍卖者和竞买者提供一个在线交流平台。本项…...

论文阅读 - Social bot detection in the age of ChatGPT: Challenges and opportunities

论文链接&#xff1a;https://www.researchgate.net/publication/371661341_Social_bot_detection_in_the_age_of_ChatGPT_Challenges_and_opportunities 目录 摘要&#xff1a; 引言 1.1. Background on social bots and their role in society 1.2. The rise of AI-gene…...

FPGA优质开源项目 - UDP RGMII千兆以太网

本文介绍一个FPGA开源项目&#xff1a;UDP RGMII千兆以太网通信。该项目在我之前的工作中主要是用于FPGA和电脑端之间进行图像数据传输。本文简要介绍一下该项目的千兆以太网通信方案、以太网IP核的使用以及Vivado工程源代码结构。 Vivado 的 Tri Mode Ethernet MAC IP核需要付…...

学C的第三十二天【动态内存管理】

相关代码gitee自取&#xff1a;C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 学C的第三十一天【通讯录的实现】_高高的胖子的博客-CSDN博客 1 . 为什么存在动态内存分配 学到现在认识的内存开辟方式有两种&#xff1a; 创建变量&#xff1a; int val …...

聊聊elasticsearch的data-streams

序 本文主要研究一下elasticsearch的data-streams data-streams 主要特性 首先data streams是由一个或者多个自动生成的隐藏索引组成的&#xff0c;它的格式为.ds-<data-stream>-<yyyy.MM.dd>-<generation> 示例.ds-web-server-logs-2099.03.07-000034&a…...

unreal engine c++ 创建tcp server, tcp client

TCP客户端 TcpConnect.h // Fill out your copyright notice in the Description page of Project Settings.#pragma once#include "CoreMinimal.h" #include "Common/UdpSocketReceiver.h" #include "GameFramework/Actor.h"DECLARE_DELEGATE…...

24届华东理工大学近5年自动化考研院校分析

今天给大家带来的是华东理工大学控制考研分析 满满干货&#xff5e;还不快快点赞收藏 一、华东理工大学 学校简介 华东理工大学原名华东化工学院&#xff0c;1956年被定为全国首批招收研究生的学校之一&#xff0c;1960年起被中共中央确定为教育部直属的全国重点大学&#…...

变量 varablie 声明- Rust 变量 let mut 声明与 C/C++ 变量声明对比分析

一、变量声明设计&#xff1a;let 与 mut 的哲学解析 Rust 采用 let 声明变量并通过 mut 显式标记可变性&#xff0c;这种设计体现了语言的核心哲学。以下是深度解析&#xff1a; 1.1 设计理念剖析 安全优先原则&#xff1a;默认不可变强制开发者明确声明意图 let x 5; …...

springboot 百货中心供应链管理系统小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;百货中心供应链管理系统被用户普遍使用&#xff0c;为方…...

【HTML-16】深入理解HTML中的块元素与行内元素

HTML元素根据其显示特性可以分为两大类&#xff1a;块元素(Block-level Elements)和行内元素(Inline Elements)。理解这两者的区别对于构建良好的网页布局至关重要。本文将全面解析这两种元素的特性、区别以及实际应用场景。 1. 块元素(Block-level Elements) 1.1 基本特性 …...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台

🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...

Reasoning over Uncertain Text by Generative Large Language Models

https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829https://ojs.aaai.org/index.php/AAAI/article/view/34674/36829 1. 概述 文本中的不确定性在许多语境中传达,从日常对话到特定领域的文档(例如医学文档)(Heritage 2013;Landmark、Gulbrandsen 和 Svenevei…...

第7篇:中间件全链路监控与 SQL 性能分析实践

7.1 章节导读 在构建数据库中间件的过程中&#xff0c;可观测性 和 性能分析 是保障系统稳定性与可维护性的核心能力。 特别是在复杂分布式场景中&#xff0c;必须做到&#xff1a; &#x1f50d; 追踪每一条 SQL 的生命周期&#xff08;从入口到数据库执行&#xff09;&#…...

Web后端基础(基础知识)

BS架构&#xff1a;Browser/Server&#xff0c;浏览器/服务器架构模式。客户端只需要浏览器&#xff0c;应用程序的逻辑和数据都存储在服务端。 优点&#xff1a;维护方便缺点&#xff1a;体验一般 CS架构&#xff1a;Client/Server&#xff0c;客户端/服务器架构模式。需要单独…...

LUA+Reids实现库存秒杀预扣减 记录流水 以及自己的思考

目录 lua脚本 记录流水 记录流水的作用 流水什么时候删除 我们在做库存扣减的时候&#xff0c;显示基于Lua脚本和Redis实现的预扣减 这样可以在秒杀扣减的时候保证操作的原子性和高效性 lua脚本 // ... 已有代码 ...Overridepublic InventoryResponse decrease(Inventor…...

Unity-ECS详解

今天我们来了解Unity最先进的技术——ECS架构&#xff08;EntityComponentSystem&#xff09;。 Unity官方下有源码&#xff0c;我们下载源码后来学习。 ECS 与OOP&#xff08;Object-Oriented Programming&#xff09;对应&#xff0c;ECS是一种完全不同的编程范式与数据架构…...

迁移科技3D视觉系统:重塑纸箱拆垛场景的智能革命

一、传统拆垛场景的困局与破局之道 在汽车零部件仓库中&#xff0c;每天有超过2万只异形纸箱需要拆垛分拣。传统人工拆垛面临三大挑战&#xff1a; 效率瓶颈&#xff1a;工人每小时仅能处理200-300件&#xff0c;且存在间歇性疲劳安全隐患&#xff1a;20kg以上重箱搬运导致年…...