算法之二叉树的遍历

161 阅读6分钟

前言

二叉树是什么

二叉树是另一种树型结构,它的特点是每个结点至多只有两棵子树(即二叉树中不存在度大于2的结点),并且,二叉树的子树有左右之分(其次序不能任意颠倒。)

二叉树的遍历方式

二叉树的遍历方式有三种主要的类型:前序遍历中序遍历后序遍历。这三种遍历方式是基于树的深度优先搜索(DFS)的。其次还有层次遍历,也称为广度优先搜索(BFS)。下面我将为大家介绍一下这四种遍历方式。 下列示例统一用如下二叉树的解构:

      A
     / \
    B   C
   / \   \
  D   E   F

前序遍历

前序遍历的顺序是:根节点 -> 左子树 -> 右子树。

示例

let root = {
  val: 'A',
  left: {
    val: 'B',
    left: {
      val: 'D',
    },
    right: {
      val: 'E',
    }
  },
  right: {
    val: 'C',
    right: {
      val: 'F',
    }
  }
};
function preOrder(root) {
  if (!root) return ;
  // 存储遍历结果的数组
  let res = [];
  // 将当前根节点的值添加到结果数组
  res.push(root.val);
  // 递归遍历左子树
  preOrder(root.left);
  // 递归遍历右子树
  preOrder(root.right);
  // 返回最终的遍历结果数组
  return res;
}
// 调用 preOrder 函数,并输出结果
console.log(preOrder(root));

如上述代码所示,我们通过构造一个preOrder的函数,然后通过递归的方式,遍历左子树和右子树,最后返回结果。总的来说前序遍历的步骤如下:

前序遍历的步骤:

  1. 访问根节点(Root): 首先访问二叉树的根节点,并将根节点的值记录下来。
  2. 递归遍历左子树: 对根节点的左子树进行前序遍历,重复上述步骤。
  3. 递归遍历右子树: 对根节点的右子树进行前序遍历,重复上述步骤。

中序遍历

中序遍历的顺序为:左子树 -> 根节点 -> 右子树

示例

function midOrder(root) {
  if (!root) return;  // 如果节点为空,直接返回
  // 递归遍历左子树
  midOrder(root.left);
  // 输出当前节点的值
  console.log(root.val);
  // 递归遍历右子树
  midOrder(root.right);
}
midOrder(tree);

上述代码中与前序遍历函数的区别在于输出节点值的位置,中序遍历将输出操作放在左子树递归之后、右子树递归之前,从而实现中序遍历的顺序。

中序遍历的步骤:

  1. 递归遍历左子树: 对根节点的左子树进行中序遍历,重复下面的步骤。
  2. 访问根节点: 访问当前根节点的值。
  3. 递归遍历右子树: 对根节点的右子树进行中序遍历,重复下面的步骤。

后序遍历

后序遍历的顺序为:左子树 -> 右子树 -> 根节点

示例

function backOrder(root) {
  if (!root) return [];  // 如果节点为空,直接返回空数组
  const result = [];
  // 递归遍历左子树
  result.push(...postOrderTraversal(root.left));
  // 递归遍历右子树
  result.push(...postOrderTraversal(root.right));
  // 访问根节点
  result.push(root.val);
  return result;
}
backOrder(tree)

上述代码与中序遍历函数的区别在于访问根节点的位置,后序遍历将访问操作放在左子树递归之后、右子树递归之后,从而实现后序遍历的顺序。

后序遍历的步骤:

  1. 递归遍历左子树: 对根节点的左子树进行后序遍历,重复下面的步骤。
  2. 递归遍历右子树: 对根节点的右子树进行后序遍历,重复下面的步骤。
  3. 访问根节点: 访问当前根节点的值。

层次遍历

层次遍历按照树的层级一层一层地进行,从上到下、从左到右遍历。在层次遍历中,同一层的节点会按照从左到右的顺序访问。

示例

// 定义二叉树广度优先搜索函数
function BFS(root) {
  // 创建一个空队列,用于存储待访问的节点
  const queue = [];
  // 将根节点推入队列
  queue.push(root);
  // 循环遍历,直到队列为空
  while (queue.length) {
    // 取出队首元素,即当前要访问的节点
    const top = queue[0];
    // 输出当前节点的值
    console.log(top);
    // 如果当前节点有左子节点,将左子节点推入队列
    if (top.left) {
      queue.push(top.left);
    }
    // 如果当前节点有右子节点,将右子节点推入队列
    if (top.right) {
      queue.push(top.right);
    }
    // 出队操作,将队首元素移出队列
    queue.shift();
  }
}
// 调用广度优先搜索函数
BFS(root);

上述代码使用队列来实现层次遍历。首先将根节点入队,然后循环遍历队列,依次取出队首元素,输出节点值,将其子节点入队,直到队列为空。最终返回按照层次遍历的顺序存储的结果数组。

层次遍历的步骤:

  1. 初始化队列: 创建一个空队列,用于存储待访问的节点。

  2. 根节点入队: 将根节点入队。

  3. 循环遍历: 使用循环,只要队列不为空,就一直执行循环体。

    • 取出队首元素: 在循环体中,取出队列的第一个元素,即当前要访问的节点。
    • 输出节点值: 输出当前节点的值,进行其他操作。
    • 子节点入队: 如果当前节点有左子节点,将左子节点入队;如果有右子节点,将右子节点入队。
    • 出队操作: 将队首元素出队。
  4. 遍历结束: 当队列为空时,遍历结束。

对比前三种方式来说,层次遍历与其他遍历方式(例如前序遍历、中序遍历、后序遍历)的主要区别在于遍历的顺序和访问节点的时机。

区别

  1. 遍历顺序:

    • 层次遍历: 按照树的层次顺序逐层访问节点。首先访问根节点,然后是第一层的所有节点,接着是第二层的所有节点,以此类推。
    • 前序遍历: 根节点 -> 左子树 -> 右子树。首先访问根节点,然后是左子树,最后是右子树。
    • 中序遍历: 左子树 -> 根节点 -> 右子树。首先访问左子树,然后是根节点,最后是右子树。
    • 后序遍历: 左子树 -> 右子树 -> 根节点。首先访问左子树,然后是右子树,最后是根节点。
  2. 访问节点的时机:

    • 层次遍历: 在取出队首元素的时候即可访问节点。
    • 前序遍历: 在递归遍历左子树之前访问节点。
    • 中序遍历: 在递归遍历左子树之后、右子树之前访问节点。
    • 后序遍历: 在递归遍历左子树和右子树之后访问节点。

总结

遍历方式总结:

  1. 层次遍历:

    • 顺序: 按照树的层次逐层访问。
    • 数据结构: 使用队列作为辅助数据结构。
    • 实现方式: 广度优先搜索,先访问根节点,然后逐层访问每个节点。
  2. 前序遍历:

    • 顺序: 根节点 -> 左子树 -> 右子树。
    • 数据结构: 通常使用递归实现,也可使用栈作为辅助数据结构。
    • 实现方式: 深度优先搜索,在递归遍历左子树之前访问节点。
  3. 中序遍历:

    • 顺序: 左子树 -> 根节点 -> 右子树。
    • 数据结构: 通常使用递归实现,也可使用栈作为辅助数据结构。
    • 实现方式: 深度优先搜索,在递归遍历左子树之后、右子树之前访问节点。
  4. 后序遍历:

    • 顺序: 左子树 -> 右子树 -> 根节点。
    • 数据结构: 通常使用递归实现,也可使用栈作为辅助数据结构。
    • 实现方式: 深度优先搜索,在递归遍历左子树和右子树之后访问节点。