- 作者:老汪软件技巧
- 发表时间:2024-09-28 10:01
- 浏览量:
扯皮
好久没写过小短文了,上班之后匀给写文章的时间非常少,通常都是碎片化时间稍微写几段,现在草稿箱里已经压了四篇文章没发了,这段时间赶紧整理一下
看到这个标题第一时间相信想到的都是 React 的 Fiber 架构,明明已经有很多篇文章解析了为什么还要写这个相关的呢?因为之前看了不少比较优秀的文章,有些会把虚拟 DOM 转 Fiber 链表的过程也进行详解,但是由于 Fiber 架构本身比较复杂,针对于这个过程并不会重点讲
这就导致一个问题,每次看完这部分内容后,只能说懂了没懂,突然有一天自己总结 Fiber 架构的时候上来先反问自己:虚拟 DOM 转 Fiber 的过程是怎样的呢?答:emm 先这样遍历一下,然后再那样,然后就成链表了...
说明还是不太懂,突然一想这不就是一个手写题么,以前树 -> 数组、数组 -> 树写的贼 6,到这个直接抓瞎了,赶紧下来总结一下
正文确定数据类型
既然是两个数据结构的转换那必然需要先确定这两个结构类型,树结构不多说了,既然链表能够描述一棵树,那它不是普通的链表,它是一个有功的三向链表,具体是哪三向呢?肯定就还是按照 React 的 fiber 来啦:
废话不多说,上 TS 写类型:
type TreeNodeType = {
id: number;
name: string;
children?: TreeNodeType[];
};
type LinkNodeType = TreeNodeType;
class LinkNode {
node: LinkNodeType | null;
return: LinkNode | null = null;
child: LinkNode | null = null;
sibling: LinkNode | null = null;
constructor(node?: LinkNodeType) {
this.node = node || null;
}
}
遍历顺序
要对树结构进行转化肯定要遍历每个树节点,我们肯定是需要一边遍历一边建立链表节点关系,关键在于是选 BFS 还是 DFS,不妨代入分析一下看看,以下面这棵树为例:
如果是 BFS 它的遍历顺序为:ABCDEFG
层序遍历上来就可以被抬走了,可以清楚的看到比较方便建立兄弟关系(sibling),毕竟都是一层紧挨着的,但是针对于父子关系(return、child)就错乱了,C -> D 明显关系错位
要想跟父子打好关系还是得靠 DFS ,DFS 又分为三种:ABDECFG(先序)、DBEAFCG(中序)、DEBFGCA(后序)还是考虑我们 return、sibling、child 的关系,以 B、D、E 三个节点为例,中序的 DBE (D、E sibling 关系)和后序的 DEB(父子关系)处理起来都不太方便
只有先序的 BDE 比较符合,但是又不完全一样,因为我们并不是先一路遍历到最底的节点再向上回头,而是先处理父节点,之后紧接着遍历处理孩子节点
先序遍历的 BDE 的顺序是符合的,但是针对于子树 ABC 不合适,一般情况下我们手写的 DFS 都是以递归的方式实现的,但在这里不只要遍历节点还要建立关系,通过这层关系能够让我们以循环的方式来实现
代码实现
根据上面的这棵二叉树来想想遍历过程中该怎么建立关系,不妨将整个二叉树拆成一棵棵子树,那么我们每轮其实就是在处理一个父节点和几个子节点之间的关系,我们先来棵只有父子关系的子树:
const tree: TreeNodeType = {
id: 1,
name: "A",
children: [
{
id: 2,
name: "B",
},
{
id: 3,
name: "C",
},
{
id: 4,
name: "D",
},
{
id: 5,
name: "E",
},
],
};
来实现一个 handleTreeChildren 来建立关系,按照 Fiber 之间的关系其 child 指向是第一个孩子节点,所以其关键在于区分处理第一个孩子节点和其余孩子节点,其余孩子节点只需要在其之间建立 sibling 关系即可:
function handleTreeChildren(treeNode: TreeNodeType, children: TreeNodeType[]) {
const headNode = new LinkNode(treeNode);
let prevNode: LinkNode | null = null;
children.forEach((node, index) => {
const linkNode = new LinkNode(node);
linkNode.return = headNode;
if (index === 0) {
headNode.child = linkNode;
} else {
prevNode!.sibling = linkNode;
}
prevNode = linkNode;
});
return headNode;
}
console.log(handleTreeChildren(tree, tree.children || []));
打印看下结果没什么毛病:
现在只是处理了一棵子树,后面要把子树串联起来,实际就是循环上面的子树处理过程,关键是怎么循环呢,需要回想一下 React Fiber 的处理过程,它的顺序是 child -> sibling -> return,也就是说我们处理完孩子节点后,需要再将 child 指向的第一个子节点重复上述过程,没有 child 就找 sibling,没有 sibling 就 return 回去找它的 sibling(俗称找叔叔♂️),我们来拓展 handleTreeChildren 的实现:
function handleTreeChildren(treeNode: TreeNodeType, children: TreeNodeType[]) {
const headNode = new LinkNode(treeNode);
let prevNode: LinkNode | null = null;
children.forEach((node, index) => {
const linkNode = new LinkNode(node);
linkNode.return = headNode;
if (index === 0) {
headNode.child = linkNode;
} else {
prevNode!.sibling = linkNode;
}
prevNode = linkNode;
});
// new
if (headNode.child) return headNode.child;
if (headNode.sibling) return headNode.sibling;
return headNode.return?.sibling;
}
除此之外就需要一个循环重复上述过程了,我们将 handleTreeChildren 中创建的 headNode 抽离至外面,以 while 循环的方式处理每棵子树:
// new
function TreeToLinkList(tree: TreeNodeType) {
const headNode = new LinkNode(tree);
let currentNode: LinkNode | null | undefined = headNode;
while (currentNode) {
const children = currentNode.node?.children || [];
currentNode = handleTreeChildren(currentNode, children);
}
return headNode;
}
function handleTreeChildren(headNode: LinkNode, children: TreeNodeType[]) {
let prevNode: LinkNode | null = null;
children.forEach((node, index) => {
const linkNode = new LinkNode(node);
linkNode.return = headNode;
if (index === 0) {
headNode.child = linkNode;
} else {
prevNode!.sibling = linkNode;
}
prevNode = linkNode;
});
if (headNode.child) return headNode.child;
if (headNode.sibling) return headNode.sibling;
return headNode.return?.sibling;
}
用上面图中的那棵二叉树来看看结果,没什么问题:
const tree: TreeNodeType = {
id: 1,
name: "A",
children: [
{
id: 2,
name: "B",
children: [
{
id: 4,
name: "D",
},
{
id: 5,
name: "E",
},
],
},
{
id: 3,
name: "C",
children: [
{
id: 6,
name: "F",
},
{
id: 7,
name: "G",
},
],
},
],
};
console.log(TreeToLinkList(tree));
到此我们树转链表的实现基本就结束啦,还有个特殊情况,真实场景下这里的树结构通常不是一个对象,而是一个数组结构里面存每个子树,需要针对于这种情况进行处理
思路其实很简单,我们可以手动创建一个带头链表,这个头节点不存数据只起链接作用,不过每次节点的 children 就需要我们手动存一下了,毕竟现在多了个带头节点,会与之前的实现错位:
function TreeToLinkList(tree: TreeNodeType | TreeNodeType[]) {
// new
const trees = Array.isArray(tree) ? tree : [tree];
const headNode = new LinkNode();
let currentNode: LinkNode | null | undefined = headNode;
let treeChildren = trees;
while (currentNode) {
currentNode = handleTreeChildren(currentNode, treeChildren || []);
treeChildren = currentNode?.node?.children || [];
}
// new
return headNode.node ? headNode : headNode.child;
}
再来棵树看看效果,没什么问题,这其实也是 React 和 Vue 要求组件必须有一个根节点的原因,哪怕没有根节点框架底层也会给你包一层形成树结构而非数组:
const tree: TreeNodeType[] = [
{
id: 1,
name: "A",
children: [
{
id: 4,
name: "D",
},
{
id: 5,
name: "E",
},
],
},
{
id: 2,
name: "B",
},
{
id: 3,
name: "C",
children: [
{
id: 6,
name: "F",
},
{
id: 7,
name: "G",
},
],
},
];
console.log(TreeToLinkList(tree));
End
全部代码如下,除去类型的话感觉 40 行不到 ,整个实现过程就已经有 React 的影子了,像 while 循环再拓展一个条件就是时间分片,根据剩余时间来执行下次循环,也正是因为采取了这种结构才能够实现 React 所说的“中断”效果:
type TreeNodeType = {
id: number;
name: string;
children?: TreeNodeType[];
};
type LinkNodeType = TreeNodeType;
class LinkNode {
node: LinkNodeType | null;
return: LinkNode | null = null;
child: LinkNode | null = null;
sibling: LinkNode | null = null;
constructor(node?: LinkNodeType) {
this.node = node || null;
}
}
function TreeToLinkList(tree: TreeNodeType | TreeNodeType[]) {
const trees = Array.isArray(tree) ? tree : [tree];
const headNode = new LinkNode();
let currentNode: LinkNode | null | undefined = headNode;
let treeChildren = trees;
while (currentNode) {
currentNode = handleTreeChildren(currentNode, treeChildren || []);
treeChildren = currentNode?.node?.children || [];
}
return headNode.node ? headNode : headNode.child;
}
function handleTreeChildren(headNode: LinkNode, children: TreeNodeType[]) {
let prevNode: LinkNode | null = null;
children.forEach((node, index) => {
const linkNode = new LinkNode(node);
linkNode.return = headNode;
if (index === 0) {
headNode.child = linkNode;
} else {
prevNode!.sibling = linkNode;
}
prevNode = linkNode;
});
if (headNode.child) return headNode.child;
if (headNode.sibling) return headNode.sibling;
return headNode.return?.sibling;
}
到此这篇小短文就结束了,别看实现的代码量不多,如果真让面试手写的话之前没有了解过 Fiber 架构还真不好撕出来,以后真能当上面试官了就搞点这些题来玩玩,代码量少不需要多长时间,还又考察了数据结构,多好的一道题