数据结构-树形结构数据的生成和花式遍历姿势

2,305 阅读4分钟

前言

衔接上一篇Node的文件操作,专门写一篇树形结构的了解是和实现 ,同时加深自己对树形结构数据的了解

什么是树

image.png

  • 线性结构 像数组 ,栈,队列,默认都是线性结构;
  • 非线性结构 树则是非线性结构,常见的树形结构有二叉树,和多叉树(大于两个叉的树) 开发中常见的树性结构 :文件目录,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() 思路:
  1. 如果根节点是 null new 一个Node 且为根节点
  2. 根节点不为空,进行while循环,设置当前节点为根节点,parent为当前节点,比较当前节点的elment是否小于 add的入参 得出compare的布尔值
  3. compare为true 变更当前节点为当前节点的右子树节点,反之设置当前节点的左子树节点为当前节点 ,直至循环结束的出最终的compare和parent
  4. 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 });

测试结果(完美)

image.png

树的遍历

深度优先遍历

  • 根节点出发 纵向遍历
  • 使用场景 react中虚拟dom的diff算法

先序遍历

  • 遍历顺序 根 左 右
  • 代码实现
 preOderTraversal() {
    function traversal(node) {
      // 递归优先写 终止条件 血的教训
      if (node === null) return;
      console.log(node.element);
      traversal(node.left);
      traversal(node.right);
    }
    traversal(this.root);
  }
  • 执行结果: image.png

中序遍历

  • 遍历顺序左 根 右
  • 代码实现
  inOderTraversal() {
    function traversal(node) {
      // 递归优先写 终止条件 血的教训
      if (node === null) return;
      traversal(node.left);
      console.log(node.element);
      traversal(node.right);
    }
    traversal(this.root);
  }
  • 执行结果:

image.png

后序遍历

  • 遍历顺序左 右 根
  • 代码实现
 postOderTraversal() {
    function traversal(node) {
      // 递归优先写 终止条件 血的教训
      if (node === null) return;
      traversal(node.left);

      traversal(node.right);
      console.log(node.element);
    }
    traversal(this.root);
  }
  • 执行结果:

image.png

广度优先遍历

  • 根节点出发 逐级遍历,每级从左到右遍历
  • 代码实现
  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);
      }
    }
  }
  • 执行结果

image.png

深度优先和广度优先的比较

  1. 各类深度优先遍历 性能上没有差别
  2. 深度优先遍历在性能上会比广度优先 差一些些
  3. 解决之前的一个小疑问 (希望对正在看的你也有所启发) 既然广度优先遍历性能比深度优先好 那为什么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 });

最后

最后如果觉得本文有帮助 记得点赞三连哦 十分感谢