Vue3 Diff 算法简易版

news2025/4/19 13:51:12

背景

学习一下Vue3中的diff算法~

逻辑概述

这个算法的过程不算太复杂:

  • 同时从新旧列表的头部遍历,找出相同的节点,则patch,否则就停止遍历;
  • 同时从新旧列表的尾部遍历,找出相同的节点,则patch,否则就停止遍历;
  • 如果旧列表的节点都遍历过了,新列表还有节点没有被遍历,那么说明新列表增加了节点,则将这些节点全部新增;
  • 如果此时旧列表还有节点没有被遍历过、新列表已经都遍历完,那么说明新列表删除了旧列表的某些节点,则将旧列表中未被遍历过的节点删除;
  • 如果新旧列表中都存在未遍历过的节点,则需要操作这些节点,这里的操作指的是新增、移动、删除,这一步骤是最复杂的,涉及到了二分查找和最长升上子序列。

Diff过程

函数传参

/**
 * Vue3 Diff简易版
 * @param {*} oldList 旧列表
 * @param {*} newList 新列表
 * @param {*} param2  4个函数分别代表 挂载、更新、移除、移动节点操作
 */
function diffArray(oldList, newList, { mountElement, patch, unmount, move }) {
	// 节点是否可以复用,这里只是简单的判断key值,事实上判断元素是否可以复用,还要判断tag等值
  	function isSameVnodeType(node1, node2) {
    	return node1.key === node2.key;
  	}
	// ...
}

从头遍历新旧列表,找出相同的节点

在这里插入图片描述
在这张图中,我们同时从两个列表的头部遍历,相同的节点有A B,然后停止了循环:

function diffArray(oldList, newList, { mountElement, patch, unmount, move }) {
  // 节点是否可以复用
  function isSameVnodeType(node1, node2) {
    return node1.key === node2.key;
  }
  
  // 头部遍历的起始变量
  let i = 0;
  // 旧列表的长度
  const oldLen = oldList.lenght;
  // 新列表的长度
  const newLen = newList.lenght;

 // 1. 从头遍历新旧列表,找出相同的节点
 while(i < oldLen && i < newLen) {
    const oldNode = oldList[i];
    const newNode = newList[i];

    if(isSameVnodeType(oldNode, newNode)) {
      patch(oldNode.key);
      i++;
    }else {
      break;
    }
  }
}

从尾遍历新旧列表,找出相同的节点

在这里插入图片描述
由于新旧列表的长度不一定相同,所以两者遍历的尾巴坐标也不一样,所以需要单独声明,找到相同的节点就继续往前走,否则就退出循环:

// 旧列表尾巴下标
const oldEndIndex = oldLen - 1;
// 新列表尾巴下标
const newEndIndex = newLen - 1; 

// 2.从尾遍历新旧列表,找出相同的节点
while(i <= oldEndIndex && i <= newEndIndex){
  const oldNode = oldList[oldEndIndex];
  const newNode = newList[newEndIndex];
  if(isSameVnodeType(oldNode, newNode)) {
    patch(oldNode.key);
    oldEndIndex--;
    newEndIndex--
  }else {
    break;
  }
}

新增节点

在这里插入图片描述
之前的i在最后循环的时候,最后的值为2,而oldEndIndex = 1, newEndIndex = 2,则说明新列表比旧列表多出了一个节点,即新增节点。

// 3.新增节点
if( i > oldEndIndex && i <= newEndIndex ){
    while(i <= newEndIndex) {
      const newNode = newList[i];
      mountElement(newNode.key);
      i++;
    }
 }

删除节点

在这里插入图片描述
如前面所说,此时旧列表还有节点没有被遍历过、新列表已经遍历完,那么说明新列表删除了旧列表的某些节点,则将旧列表中未被遍历过的节点删除,由图可知,C节点在新列表中被删除了。

// 4. 删除节点
else if(i <= oldEndIndex && i > newEndIndex) {
  while(i <= oldEndIndex) {
    const delNode = oldList[i];
    unmount(delNode.key);
    i++;
  }
}

到这里的代码整体如下:

/**
 * Vue3 Diff简易版
 * @param {*} oldList 旧列表
 * @param {*} newList 新列表
 * @param {*} param2  4个函数分别代表 挂载、更新、移除、移动节点操作
 */
function diffArray(oldList, newList, { mountElement, patch, unmount, move }) {
  // 节点是否可以复用
  function isSameVnodeType(node1, node2) {
    return node1.key === node2.key;
  }

  let i = 0;
  // 旧列表的长度
  const oldLen = oldList.lenght;
  // 新列表的长度
  const newLen = newList.lenght;
  // 旧列表尾巴下标
  const oldEndIndex = oldLen - 1;
  // 新列表尾巴下标
  const newEndIndex = newLen - 1; 

  // 1. 从头遍历新旧列表,找出相同的节点
  while(i < oldLen && i < newLen) {
    const oldNode = oldList[i];
    const newNode = newList[i];

    if(isSameVnodeType(oldNode, newNode)) {
      patch(oldNode.key);
      i++;
    }else {
      break;
    }
  }

  // 2.从尾遍历新旧列表,找出相同的节点
  while(i <= oldEndIndex && i <= newEndIndex){
    const oldNode = oldList[oldEndIndex];
    const newNode = newList[newEndIndex];
    if(isSameVnodeType(oldNode, newNode)) {
      patch(oldNode.key);
      oldEndIndex--;
      newEndIndex--
    }else {
      break;
    }
  }

  // 3.新增节点
  if( i > oldEndIndex && i <= newEndIndex ){
    while(i <= newEndIndex) {
      const newNode = newList[i];
      mountElement(newNode.key);
      i++;
    }
  }

  // 4.删除节点
  else if(i <= oldEndIndex && i > newEndIndex) {
    while(i <= oldEndIndex) {
      const delNode = oldList[i];
      unmount(delNode.key);
      i++;
    }
  }

  else {
    // ...
   } 
}

最后的处理

好了接下来进入最烧脑的环节,先给个例子先:
在这里插入图片描述
到这里我们其实就是要将新列表中的[M, H,.C, D, E, J]和旧列表中的[C, E, H]进行对比从而做出操作。源码肯定不会那么傻先遍历新列表的节点,再内嵌循环旧列表节点一个个做对比,他们根据数组的特点,转换了数据结构,减少了循环的耗时。接下来跟着源码学习:

// 新旧节点不同的起点坐标
const oldStartIndex = i;
const newStartIndex = i;

// 5.1 根据数组的特点,将新列表中的key和index做成映射关系
const keyToNewIndexMap = new Map();
for(let i = newStartIndex; i < newEndIndex; i++){
	const node = newList[i];
	keyToNewIndexMap.set(node.key, i);
}

得出来的结果如下:

在这里插入图片描述
这里先提一下相对坐标!!!
Vue源码中为了方便,直接将循环的范围变成了我们要操作的新列表的这些节点中(即下面会用到相对坐标),比如M的相对要操作的节点列表的坐标是0,而不是原来列表中的2。
继续,接下来声明一些我们后面会用到的变量:

// 新列表中需要操作的节点数量(上述例子就是6个节点)
const toBePatched = newEndIndex - newStartIndex + 1;
// 已经操作过的节点数量
let patched = 0;
// 先将这个参数简单理解为,新列表的节点与旧列表节点的映射,如果存在就非0,如果不存在就是0
const newIndexToOldIndexMap = new Array(toBePatched).fill(0);

然后我们现在先遍历旧列表:

for(let i = oldStartIndex; i < oldEndIndex; i++){
	const node = oldList[i];
	
	// 如果需要操作的节点数量已经小于操作过的节点,说明旧列表中这些节点是木有用的,需要卸载
	if(patched >= toBePatched){
		unmount(node.key);
		continue;
	}
	
	// 接下来根据oldNode中的key,来看看keyToNewIndexMap是否有对应的下标,如果有,那说明旧节点在新列表中被复用啦,否则就得删除
	// Vue3源码中有一段是处理节点没有key的情况,这里就不写啦,这里默认我们的节点都是有key的
	const newIndex = keyToNewIndexMap.get(node.key);
	
	if(newIndex === undefined){
		// 旧节点在新列表中没有被复用,直接卸载
		unmount(node.key);
	}else {
		// 如果在旧列表中找到可以复用的节点,那么更新newIndexToOldIndexMap[相对坐标] = 旧列表的坐标 + 1
		// 这里加1的原因 后面再解释!
		newIndexToOldIndexMap[newIndex - newStartIndex] = i + 1;
		// 复用节点
		patch(node.key);
		// 操作过的节点数量 + 1
		patched++;
	}
}

那么这里我们先得出newIndexToOldIndexMap的值:
在这里插入图片描述
newIndexToOldIndexMap[相对坐标] = 旧列表的坐标 + 1这里加一的原因我理解的是,这个数组如果第一个元素在新旧列表都存在,那么它的坐标就是0,所以为了区分是否存在,需要+1,确保称为非0数。早期看技术文,也有初始化为-1的情况,这种就不用+1了。
那么这里就获取到了一个数组坐标,接下来的精华就是根据这个数组坐标,来对我们的节点进行操作,操作之前需要先获得最长升上子序列,好了这个算法我们先跳过实现过程(算法解说不适合我),先说一下为什么是最长升上子序列。我们在操作节点的时候,是不是希望有比较多的节点保持位置不变,尽量改变较少的节点。
我们得出的数组是[0, 5, 3, 0, 4, 0],忽略0(不是复用的节点)这里我们不难看出来连续的最长子序列是[3, 4]对应的是节点C、E,我们只要移动节点D即可。
不过这里最长子序列的结果应该返回的是[3, 4]的坐标,所以结果应该是[2, 4]
在这里插入图片描述

// 先假设获得最长升上子序列
function getSequence(arr){
	// 这里应该返回的是序列对应的下标,[3, 4] 转为下标变成[2, 4]
	return [2, 4]
}

然后继续我们的逻辑:

for(let i = oldStartIndex; i < oldEndIndex; i++){
	// ...
}

// 最长升上子序列
const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap);
// 最长升上子序列的最后一个坐标
let lastIndex = increasingNewIndexSequence.length - 1;
/**
* 由于移动元素可能会用到inertBefore方法
* 该方法需要知道后一个元素,才能插入前面一个元素
* 所以这次遍历需要从后面开始
*/
for (i = toBePatched - 1; i >= 0; i--){
	if(newIndexToOldIndexMap[i] === 0) {
		// 说明是新增节点
		mountElement(node.key);
	}else {
		// 将相对坐标转为绝对(列表)坐标
		const index = newStartIndex + i;
		// 获得对应新列表中的节点
		const node = newList[index];
		// 如果没有最长上升子序列 或者 当前节点不在该子序列中,则需要移动
		if(lastIndex < 0 || i !== increasingNewIndexSequence[lastIndex]) {
			move(node.key)
		}else {
			lastIndex--;
		}
	}
	
}

在这里插入图片描述

newIndexToOldIndexMap从后往前遍历:

  • 0代表新增节点,向前移动
  • 当前i为4, increasingNewSequence[lastIndex] = 4,相等,向前移动,lastIndex–
  • 0代表新增节点,向前移动
  • 当前i为2, increasingNewSequence[lastIndex] = 2,相等,向前移动,lastIndex–
  • 由于lastIndex小于0,所以移动节点5
  • 0代表新增节点,向前移动,结束循环

这里有个优化的点,就是获得最长升上子序列这个函数其实挺耗时的,我们在某些情况下,其实并不一定要使用它:

// 是否需要获得最长升上子序列(是否需要移动节点)
let move = false;
// 子序列中最大的值
let maxNewIndexSoFar = 0;

for(let i = oldStartIndex; i < oldEndIndex; i++){
	// ...
	if(newIndex === undefined) {
        // ...
      }else {
        // 判断是否有人插队(每个节点都按需递增的话)
        if(newIndex >= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex;
        }else {
          // 说明被插队了,得去求最长递增子序列
          move = true;
        }
        // ...
      }
}
// 获取最长子序列(可能很耗时,所以要进行优化,判断是否确定要move)
const increasingNewIndexSequence = move ? getSequence(newIndexToOldIndexMap) : [];

所以最后的代码就是:

/**
 * Vue3 Diff简易版
 * @param {*} oldList 旧列表
 * @param {*} newList 新列表
 * @param {*} param2  4个函数分别代表 挂载、更新、移除、移动节点操作
 */
function diffArray(oldList, newList, { mountElement, patch, unmount, move }) {
  // 节点是否可以复用
  function isSameVnodeType(node1, node2) {
    return node1.key === node2.key;
  }

  let i = 0;
  // 旧列表的长度
  const oldLen = oldList.lenght;
  // 新列表的长度
  const newLen = newList.lenght;
  // 旧列表尾巴下标
  const oldEndIndex = oldLen - 1;
  // 新列表尾巴下标
  const newEndIndex = newLen - 1; 

  // 1. 从头遍历新旧列表,找出相同的节点
  while(i < oldLen && i < newLen) {
    const oldNode = oldList[i];
    const newNode = newList[i];

    if(isSameVnodeType(oldNode, newNode)) {
      patch(oldNode.key);
      i++;
    }else {
      break;
    }
  }

  // 2.从尾遍历新旧列表,找出相同的节点
  while(i <= oldEndIndex && i <= newEndIndex){
    const oldNode = oldList[oldEndIndex];
    const newNode = newList[newEndIndex];
    if(isSameVnodeType(oldNode, newNode)) {
      patch(oldNode.key);
      oldEndIndex--;
      newEndIndex--
    }else {
      break;
    }
  }

  // 3.新增节点
  if( i > oldEndIndex && i <= newEndIndex ){
    while(i <= newEndIndex) {
      const newNode = newList[i];
      mountElement(newNode.key);
      i++;
    }
  }

  // 4.删除节点
  else if(i <= oldEndIndex && i > newEndIndex) {
    while(i <= oldEndIndex) {
      const delNode = oldList[i];
      unmount(delNode.key);
      i++;
    }
  }

  else {
    // 新旧节点不同的起点坐标
    const oldStartIndex = i;
    const newStartIndex = i;
    
    // 5.1 根据数组的特点,将新列表中的key和index做成映射关系
    const keyToNewIndexMap = new Map();
    for(let i = newStartIndex; i < newEndIndex; i++) {
      const node = newList[i];
      keyToNewIndexMap.set(node.key, i);
    }

    // 新列表中需要操作的节点数量(上述例子就是6个节点)
    const toBePatched = newEndIndex - newStartIndex + 1;
    // 已经操作过的节点数量
    let patched = 0;
    // 先将这个参数简单理解为,新列表的节点与旧列表节点的映射,如果存在就非0,如果不存在就是0
    const newIndexToOldIndexMap = new Array(toBePatched).fill(0);
    // 是否需要获得最长升上子序列(是否需要移动节点)
    let moved = false;
    // 子序列中最大的值
    let maxNewIndexSoFar = 0;

    for(let i = oldStartIndex; i < oldEndIndex; i++) {
      const node = oldList[i];

      // 如果需要操作的节点数量已经小于操作过的节点,说明旧列表中这些节点是木有用的,需要卸载
      if(patched >= toBePatched) {
        unmount(node.key);
        continue;
      }

      // 接下来根据oldNode中的key,来看看keyToNewIndexMap是否有对应的下标,如果有,那说明旧节点在新列表中被复用啦,否则就得删除
      // Vue3源码中有一段是处理节点没有key的情况,这里就不写啦,这里默认我们的节点都是有key的
      const newIndex = keyToNewIndexMap.get(node.key);

      if(newIndex === undefined) {
        // 旧节点在新列表中没有被复用,直接卸载
        unmount(node.key);
      }else {
        // 判断是否有人插队(每个节点都按需递增的话)
        if(newIndex >= maxNewIndexSoFar) {
          maxNewIndexSoFar = newIndex;
        }else {
          // 说明被插队了,得去求最长递增子序列
          moved = true;
        }
        // 如果在旧列表中找到可以复用的节点,那么更新newIndexToOldIndexMap[相对坐标] = 旧列表的坐标 + 1
        newIndexToOldIndexMap[newIndex - newStartIndex] = i + 1;
        // 复用节点
        patch(node.key);
        // 操作过的节点数量 + 1
        patched++;
      }
    }

    // 获取最长子序列(可能很耗时,所以要进行优化,判断是否确定要move)
    const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : [];
    let lastIndex = increasingNewIndexSequence.length - 1;
    
    /**
    * 由于移动元素可能会用到inertBefore方法
    * 该方法需要知道后一个元素,才能插入前面一个元素
    * 所以这次遍历需要从后面开始
    */
    for(let i = toBePatched - 1; i > 0; i--) {
      if(newIndexToOldIndexMap[i] === 0) {
        // 判断节点是不是新增的,不能被复用,即新增节点
        mountElement(node.key);
      }else {
        // 将相对坐标转为绝对(列表)坐标
        const index = newStartIndex + i;
        // 获得对应新列表中的节点
        const node = newList[index];
        // 如果没有最长上升子序列 或者 当前节点不在该子序列中,则需要移动
        if(lastIndex < 0 || i !== increasingNewIndexSequence[lastIndex]) {
          move(node.key)
        }else {
          lastIndex--;
        }
      }
    }
  }
}

参考链接

  • 解锁vue3 diff算法
  • React、Vue2、Vue3的三种Diff算法
  • 300. 最长递增子序列

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/723894.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Jetpack Compose中的LazyHorizontalGrid构建灵活的水平网格布局

在这篇博客中&#xff0c;我们将探讨如何使用Jetpack Compose中的LazyHorizontalGrid构建灵活的水平网格布局。 什么是LazyHorizontalGrid&#xff1f; LazyHorizontalGrid是Jetpack Compose中的一个组件&#xff0c;它可以用来创建一个灵活的、可滚动的水平网格布局。这个组件…

django 自定义分页类和使用总结

一、关于为何要分页 当处理大量数据时&#xff0c;如果一次将这些数据查询出来进行响应&#xff0c;必然对服务器内存、负载有所影响&#xff0c;影响低接口响应&#xff0c;进而影响用户体验。 常见的方式是将数据分段展示给用户&#xff0c;如果当前分段中没有需要的数据&am…

Leetcode:84. 柱状图中最大的矩形(单调栈C++)

目录 84. 柱状图中最大的矩形 问题描述&#xff1a; 实现代码与解析&#xff1a; 单调栈 原理思路&#xff1a; 84. 柱状图中最大的矩形 问题描述&#xff1a; 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。…

Redis 7 十大数据类型

10大数据结构图示 10大数据类型&#xff1a; redis字符串(String)redis列表(ist)redis哈希表(Hash)redis集合(Set)redis有序集合(ZSet)redis地理空间(GEO)redis基数统计(HyperLogLog)redis位图(bitmap)redis位域(bitfield)redis流(Stream) Redis键(key) 命令不区分大小写&am…

数据结构-堆的创建(详解)

目录 堆的概念及结构 堆的实现 堆的向上调整算法 代码实现&#xff1a; 思路详解&#xff1a; 堆的向下调整算法 代码实现&#xff1a; 思路详解&#xff1a; 堆的创建&#xff1a; 堆结构的定义及相关函数的声明&#xff1a; 堆的初始化&#xff1a; 堆的销毁&#x…

如何减少图片大小内存?压缩图片内存大小的方法介绍

什么时候需要压缩图片内存大小 随着移动设备的普及和社交媒体的盛行&#xff0c;我们经常需要通过分享图片来传达信息和与他人互动。然而&#xff0c;有时候图片的文件大小过大&#xff0c;可能会导致上传、下载或发送过程中遇到问题。以下是一些常见情况下需要压缩图片内存大…

插值应用案例2

案例1 高点和高程 在一丘陵地带测量高程&#xff0c;x和y方向每隔100m测一个点&#xff0c;得到高程如下表所列&#xff0c;试插值一曲面&#xff0c;确定合适的模型&#xff0c;并由此测到最高点和相应的高程。 x0/z0\y0 100 200 300 400 500 100 636 697 624 478 …

Android GlSurfaceView 入门教程 : 绘制一个三角形

1. GlSurfaceView是什么 GlSurfaceView是Android中的一个类&#xff0c;继承自SurfaceView&#xff0c;用于显示OpenGL ES图形渲染的一个视图。 OpenGL ES是一种跨平台的图形API&#xff0c;用于渲染2D和3D图形&#xff0c;也可以将相机的画面显示到GlSurfaceView上&#xff0…

Spring Boot 中的 SQL 注入攻击是什么,原理,如何预防

Spring Boot 中的 SQL 注入攻击是什么&#xff0c;原理&#xff0c;如何预防 随着互联网的发展&#xff0c;Web 应用程序的数量不断增加&#xff0c;而 SQL 注入攻击也成为了常见的网络安全问题之一。SQL 注入攻击是通过在 Web 应用程序中注入恶意的 SQL 代码&#xff0c;从而…

@RestController 和 @Controller的区别?

Controller 返回一个页面单独使用 Controller 不加 ResponseBody的话一般使用在要返回一个视图的情况&#xff0c;这种情况属于比较传统的Spring MVC 的应用&#xff0c;对应于前后端不分离的情况 RestController 返回JSON 或 XML 形式数据 但RestController只返回对象&#…

FPGA综合设计实验:基于PWM脉宽调制的呼吸流水灯设计

目录 一、引言 二、项目准备 1.项目预期目标 2.项目原理及总体实现思路 三、项目模块设计 1.顶层模块 2.按键控制模块 3.呼吸灯模块 4.数码管显示模块 5.二进制转BCD码模块 四、项目测试 1.仿真测试 2.实物测试 五、项目总结 1.选题思考与过程反思 2.设计的具体…

MySQL之常见的CRUD面试题【上】

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于MySQL数据库的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 导读&#xff1a; 一.数据库的连表…

如何获得免费英文文献翻译呢?

我偶然之间发现的一个小技巧分享给大家&#xff01; 这个方法很简单&#xff0c;巧妙地运用了某外国浏览器自带的翻译功能&#xff0c;所以我们需要的是某外国浏览器 1.将需要翻译的pdf文档保存 2.进入http://pdfdo.com/pdf-to-html.aspx将PDF转为网页 3. 上传文件后耐心等待转…

web学习--SpringMVC--1 基础学习

写在前面&#xff1a; 所有的web学习基于springboot项目&#xff0c;而不会去单独的使用spring来进行。 文章目录 SpringMVC介绍原理MVC模式 入门使用导入依赖编写controller类 详细介绍注解详解ControllerRequestMappingResponseBodyRestControllerRequestParamRequestBodyR…

聚观早报 | 字节跳动要造机器人;苹果已开发悬空虚拟键盘

今日要闻&#xff1a;字节跳动要造机器人&#xff1b;苹果已开发悬空虚拟键盘&#xff1b;苹果汽车或售价9万美元&#xff1b;全球首例猪心脏移植患者仅存活60天&#xff1b;首款搭载ChatGPT的自行车问世 字节跳动要造机器人 7 月 3 日消息&#xff0c;「机器人」作为未来科技…

MySQL-分库分表详解(四)

♥️作者&#xff1a;小刘在C站 ♥️个人主页&#xff1a; 小刘主页 ♥️努力不一定有回报&#xff0c;但一定会有收获加油&#xff01;一起努力&#xff0c;共赴美好人生&#xff01; ♥️学习两年总结出的运维经验&#xff0c;以及思科模拟器全套网络实验教程。专栏&#xf…

记录安装ESXI-6.7遇到的问题(DELL)

从官网下载完ISO镜像,使用rufus制作启动盘 安装过程中报错信息 缺少网卡驱动 安装打包网卡驱动请查看此链接:https://www.cnblogs.com/Sunzz/p/11438066.html 通过检测网卡驱动后,报错信息 解决办法 关闭BIOS中的Secure Boot 具体步骤如下&#xff1a; 1.按F2进入bios 2…

最长回文子串 (力扣) 动态规划 JAVA

给你一个字符串 s&#xff0c;找到 s 中最长的回文子串。 如果字符串的反序与原始字符串相同&#xff0c;则该字符串称为回文字符串。 示例 1&#xff1a; 输入&#xff1a;s “babad” 输出&#xff1a;“bab” 解释&#xff1a;“aba” 同样是符合题意的答案。 示例 2&#…

FlinkSQL 解析字符串+行转列

近期遇到一个实时flinksql需求&#xff0c;需要根据ids数组字段解析成名称数组字段。。。 其中parent_path存放的内容是点号分割的字符串"1659077318807721985.1659120595539924993.1659121050219255810" 第一步&#xff1a;新建kafka source源 create TEMPORARY t…