浅谈作为前端开发对二叉树遍历的理解

二叉树是一种计算机基础数据结构,最近在写树组件的时候有用到了这个基本知识,这里总结一下。

1. 引

它的实际使用场景非常广泛。以下是一些常见的二叉树应用场景:

  • 文件系统:文件系统的目录结构可以使用二叉树表示,每个节点代表一个文件或目录,通过左子节点和右子节点连接形成层级结构。
  • 数据库索引:数据库索引通常使用二叉树实现,比如二叉搜索树(BST)用于快速搜索和检索数据。
  • 表达式解析和计算:二叉树可以用于解析和计算表达式,如数学表达式. 布尔表达式等。
  • 图像处理:在图像处理中,二叉树常用于图像压缩算法(如哈夫曼编码)和图像渐进式加载。
  • 路由算法:在网络路由中,二叉树可以用于构建路由表,帮助确定数据包的传输路径。
  • 解析树:二叉树可以用于构建解析树,用于解析和理解自然语言. 编程语言等结构化数据。
  • 人工智能和决策树:决策树是一种特殊的二叉树,常用于人工智能中的分类和决策问题。
  • 堆和优先队列:二叉堆是一种特殊的二叉树结构,用于实现优先队列和堆排序算法。
  • 编译器和解释器:在编译器和解释器中,二叉树常用于语法分析和构建抽象语法树(AST)。
  • 数据压缩:二叉树常用于数据压缩算法,如霍夫曼编码(Huffman Coding),通过树的构建和路径编码来实现数据的高效压缩和解压缩。
  • 线段树:线段树是一种特殊的二叉树,用于处理区间查询问题,如区间最小值. 区间和等。
  • 索引结构:二叉树可用于构建各种索引结构,如B树和B+树,用于快速的数据库查询和索引操作。
  • 线索二叉树:线索二叉树是一种优化的二叉树结构,通过添加额外的线索信息,可以快速进行中序遍历. 前序遍历等操作。
  • 缓存实现:LRU Cache(最近最少使用缓存)常使用双向链表和二叉树(如红黑树)相结合的方式实现,以快速插入. 删除和查找最近使用的数据。

二叉树一个重要的算法就是他的的遍历,在AST编译器开发. 路由导航. 文件系统等等业务场景中都在广泛的使用。

2. 构建一个二叉树

二叉树有一个根节点,所有的父节点最多只能有两个孩子。我们可以构造一个二叉树如下:

image.png

代码结构如下:

const tree = {
  name: 1,
  children: [
    {
      name: 2,
      children: [
        {
          name: 4,
          children: [{ name: 8 }],
        },
        { name: 5 },
      ],
    },
    {
      name: 3,
      children: [{ name: 6 }, { name: 7 }],
    },
  ],
};

接下来结合图形,用代码来实现一下他的遍历:

3. 遍历

层次遍历

按照层级遍历

算法思路:

  • 建立一个队列,从根节点开始,将其子节点的子节点从左至右入队
  • 遍历队列并出队,重复第二步后半部分
  • 队列为空则遍历结束

image.png

function traverse(node) {
  if (!node) {
    return;
  }

  const queue = [];
  queue.unshift(node);

  while (queue.length > 0) {
    const printNode = queue.pop();
    console.log(printNode.name);
    if (printNode.children && printNode.children.length > 0) {
      queue.unshift(...printNode.children.slice().reverse());
    }
  }
}

先序遍历

递归实现

  • 从根节点开始
  • 将左右节点当做子树,先遍历左节点,再遍历右节点
  • 直到当前节点没有子节点为止
function preOrderTraverse(root) {
  if (root != null) {
    const children = root.children || [];
    console.log(root.name);
    preOrderTraverse(children[0]);
    preOrderTraverse(children[1]);
  }
}

非递归实现

  • 建立一个栈,入栈根节点
  • 出栈当前节点,将当前节点子节点集合逆向入栈
  • 重复第二步,直到栈空

image.png

function preOrderTraverse(node) {
  if (!node) return;

  const track = [];
  track.push(node);

  while (track.length > 0) {
    const node = track.pop();
    console.log(node.name);
    const children = node.children;
    if (children && children.length > 0) {
      track.push(...children.reverse());
    }
  }
}

思考:学会了先序遍历,相信你应该能够很容易的写出后序遍历的实现!

中序遍历

非递归实现

  • 建立一个栈,入栈根节点
  • 若有左子节点,则入栈,直到没有左子节点时出栈栈顶元素并打印,入栈栈顶元素的右子节点
  • 重复第二步,直到没有可用元素且栈空为止

image.png

function middleOrderTraverse(node) {
  if (!node) return;

  const stack = [];
  let currentNode = node;
  while (currentNode || stack.length > 0) {
    if (currentNode) {
      stack.push(currentNode);
      currentNode =
        currentNode && currentNode.children ? currentNode.children[0] : null;
    } else {
      const poped = stack.pop();
      console.log(poped.name);
      currentNode = poped.children ? poped.children[1] : null;
    }
  }
}

链表实现

链表不适合做广度优先遍历,下面示意图中,深度优先会非常的方便。我们拿 react fiber 的结构来举例。

image.png

  • 遍历思路:从根节点开始,以此遍历每一个经过的节点的 child 和 sibling

伪代码实现:

function linkedListTraverse(root) {
  let wip = root;

  while (wip) {
    console.log(wip);

    if (wip.child) {
      temp = wip.child;
      wip.child = null; // 遍历过就剔除
      wip = temp;
      continue;
    }

    if (wip.sibling) {
      temp = wip.sibling;
      wip.sibling = null; // 遍历过就剔除
      wip = temp;
      continue;
    }

    wip = wip.return;
  }
}

Was this page helpful?