前言
衔接上一篇Node的文件操作,专门写一篇树形结构的了解是和实现 ,同时加深自己对树形结构数据的了解
什么是树
- 线性结构 像数组 ,栈,队列,默认都是线性结构;
- 非线性结构 树则是非线性结构,常见的树形结构有二叉树,和多叉树(大于两个叉的树) 开发中常见的树性结构 :文件目录,DOM 结构,react的的虚拟Dom,路由的配置等等(此处不深入探讨)
树的常见概念
-
节点:
根结点,父节点,字节点,兄弟节点
-
子树:
左子树,右子树,子树的个数陈为度
-
叶子节点:
度为 o 的节点(没有自己的子树)
-
非叶子节点:
度不为 0 的节点(有自己的子树)
-
节点(某个节点)的深度:
从根节点到当前子节点要经过的节点总数
-
节点的高度:
从当前节点到最远叶子节点要经过的节点总数
-
树的层数:
树的高度 ,树的深度
树的分类
- 二叉树
- 多叉树
本文仅对二叉树进行探讨
二叉树实现
- 经典案例:对数组进行二叉树转换,左子树为比根节点小的树,右节点为比根节点大的树等等(我也不会)
代码实现
生成树节点
class Node {
constructor(element, parent) {
// 目标节点
this.element = element;//当前节点
this.parent = parent;//父节点
this.left = null;
this.right = null;
}
}
生成树
constructor初始化 root节点为null
class Tree {
constructor() {
// 默认根节点 为null
this.root = null;
}
}
- 节点add() 思路:
- 如果根节点是 null new 一个Node 且为根节点
- 根节点不为空,进行while循环,设置当前节点为根节点,parent为当前节点,比较当前节点的elment是否小于 add的入参 得出compare的布尔值
- compare为true 变更当前节点为当前节点的右子树节点,反之设置当前节点的左子树节点为当前节点 ,直至循环结束的出最终的compare和parent
- new Node(element, parent),根据compare 决定是父节点的右节点还是左节点
代码实现:
add(element) {
if (this.root === null) {
// 1:如果根节点是 null new 一个Node 并且 当前节点为根节点
return (this.root = new Node(element));
}
// 更新当前节点
let currentNode = this.root; //
let parent;
let compare;
while (currentNode) {
compare = currentNode.element < element;
parent = currentNode; //遍历前记录节点
if (compare) {
//做比较 更新节点
// 为true接着以右边为根节点
currentNode = currentNode.right;
} else {
// 为true接着以左边为根节点
currentNode = currentNode.left;
}
}
//compare;放左还是放右边
// parent;放到谁的身上
let node = new Node(element, parent);
if (compare) {
parent.right = node;
} else {
parent.left = node;
}
}
测试
let tree = new Tree();
[10, 9, 8, 16, 22, 30, 20].forEach((item) => {
tree.add(item);
});
console.dir(tree, { depth: 100 });
测试结果(完美)
树的遍历
深度优先遍历
- 根节点出发 纵向遍历
- 使用场景 react中虚拟dom的diff算法
先序遍历
- 遍历顺序 根 左 右
- 代码实现
preOderTraversal() {
function traversal(node) {
// 递归优先写 终止条件 血的教训
if (node === null) return;
console.log(node.element);
traversal(node.left);
traversal(node.right);
}
traversal(this.root);
}
- 执行结果:
中序遍历
- 遍历顺序左 根 右
- 代码实现
inOderTraversal() {
function traversal(node) {
// 递归优先写 终止条件 血的教训
if (node === null) return;
traversal(node.left);
console.log(node.element);
traversal(node.right);
}
traversal(this.root);
}
- 执行结果:
后序遍历
- 遍历顺序左 右 根
- 代码实现
postOderTraversal() {
function traversal(node) {
// 递归优先写 终止条件 血的教训
if (node === null) return;
traversal(node.left);
traversal(node.right);
console.log(node.element);
}
traversal(this.root);
}
- 执行结果:
广度优先遍历
- 根节点出发 逐级遍历,每级从左到右遍历
- 代码实现
levelOrderTraversal(cb) {
// 如果想对遍历的树 的同时 对 节点进行操作 传入cb 并以当前节点 做参数 传给cb (例如二叉树的翻转)
let stack = [this.root];
let index = 0;
let currentNode;
// console.log("广度优先遍历");
while ((currentNode = stack[index++])) {
cb(currentNode);
// console.log(currentNode.element);
if (currentNode.left) {
stack.push(currentNode.left);
}
if (currentNode.right) {
stack.push(currentNode.right);
}
}
}
- 应用场景:二叉树翻转,webpack中对ast树的操作 各种插件对ast树的操作
广度优先遍历的使用场景 二叉树翻转
- 代码实现
reverseTree(cb) {
let stack = [this.root];
let index = 0;
let currentNode;
// console.log("广度优先遍历");
while ((currentNode = stack[index++])) {
let temp = currentNode.left;
currentNode.left = currentNode.right;
currentNode.right = temp;
if (currentNode.left) {
stack.push(currentNode.left);
}
if (currentNode.right) {
stack.push(currentNode.right);
}
}
}
- 执行结果
深度优先和广度优先的比较
- 各类深度优先遍历 性能上没有差别
- 深度优先遍历在性能上会比广度优先 差一些些
- 解决之前的一个小疑问 (希望对正在看的你也有所启发) 既然广度优先遍历性能比深度优先好 那为什么dom diff不用广度优先区 做比较 首先:dom tree的解析是由上至下的 其次:虚拟dom的遍历用广度优先遍历不符合编程思维,例如,A1,A2,A3三个同级组件,理应先执行完A1 及其子组件,否则会引起层级错乱 最后:我想从空间复杂度角度说下 广度优先遍历保留全部结点,占用空间大
完整代码
class Node {
constructor(element, parent) {
// 目标节点
this.element = element;
this.parent = parent;
this.left = null;
this.right = null;
}
}
class Tree {
constructor() {
// 默认根节点 为null
this.root = null;
}
add(element) {
if (this.root === null) {
return (this.root = new Node(element));
}
let currentNode = this.root; //
let parent;
let compare;
while (currentNode) {
compare = currentNode.element < element;
parent = currentNode; //遍历前记录节点
if (compare) {
currentNode = currentNode.right;
} else {
currentNode = currentNode.left;
}
}
let node = new Node(element, parent);
if (compare) {
parent.right = node;
} else {
parent.left = node;
}
}
// 先序遍历 ---常规写法 :递归 想优化可以用栈 但我没有写过哈
preOderTraversal() {
function traversal(node) {
// 递归优先写 终止条件 血的教训
if (node === null) return;
console.log(node.element);
traversal(node.left);
traversal( node.right);
}
traversal(this.root);
}
// 中序遍历
inOderTraversal() {
function traversal(node) {
// 递归优先写 终止条件 血的教训
if (node === null) return;
traversal(node.left);
console.log(node.element);
traversal(node.right);
}
traversal(this.root);
}
// 后序遍历
postOderTraversal() {
function traversal(node) {
// 递归优先写 终止条件 血的教训
if (node === null) return;
traversal(node.left);
traversal(node.right);
console.log(node.element);
}
traversal(this.root);
}
levelOrderTraversal(cb) {
// r如果想对遍历的树 的同时 对 节点进行操作 传入cb 并以当前节点 做参数 传给cb
let stack = [this.root];
let index = 0;
let currentNode;
// console.log("广度优先遍历");
while ((currentNode = stack[index++])) {
cb(currentNode);
// console.log(currentNode.element);
if (currentNode.left) {
stack.push(currentNode.left);
}
if (currentNode.right) {
stack.push(currentNode.right);
}
}
}
reverseTree(cb) {
// 如果想对遍历的树 的同时 对 节点进行操作 传入cb 并以当前节点 做参数 传给cb
let stack = [this.root];
let index = 0;
let currentNode;
// console.log("广度优先遍历");
while ((currentNode = stack[index++])) {
let temp = currentNode.left;
currentNode.left = currentNode.right;
currentNode.right = temp;
if (currentNode.left) {
stack.push(currentNode.left);
}
if (currentNode.right) {
stack.push(currentNode.right);
}
}
}
}
let tree = new Tree();
[10, 9, 8, 16, 22, 30, 20].forEach((item) => {
tree.add(item);
});
tree.levelOrderTraversal
console.dir(tree, { depth: 100 });
tree.preOderTraversal();
console.dir(tree, { depth: 100 });
tree.inOderTraversal();
tree.postOderTraversal();
tree.levelOrderTraversal((node) => {
// 例子 :webpack中对ast树的操作 各种插件对ast树的操作
node.element *= 2;
// console.log("当前节点的,element", node.element);
});
console.dir(tree, { depth: 100 });
tree.reverseTree();
console.dir(tree, { depth: 100 });
最后
最后如果觉得本文有帮助 记得点赞三连哦 十分感谢