二叉树遍历算法学习笔记:递归与迭代实现

48 阅读8分钟

树是数据结构中核心的非线性结构,相较于数组、链表等线性结构,它能高效组织具有层级关系的数据(如文件系统、DOM 树、数据库索引)。二叉树作为树结构中最基础、应用最广泛的类型,其遍历操作是掌握二叉树的核心 —— 遍历的本质是按特定顺序访问二叉树的每个节点且仅访问一次。本文将围绕二叉树的核心概念,详细讲解前序、中序、后序(递归实现)和层序(迭代实现)四种遍历方式的思路与 JavaScript 代码实现。

一、树与二叉树的核心概念

1.1 树的基础概念

树是由 n(n≥0) 个节点组成的有限集合,满足两个核心条件:

  • 当 n=0 时,为空树;
  • 当 n>0 时,有且仅有一个根节点(Root),其余节点可分为若干个互不相交的子树,每个子树本身也是一棵树。

树的关键术语需重点理解:

  • 层次:根节点层次为 1,子节点层次为父节点层次 + 1;
  • 高度:从叶子节点向上计数,每一层高度 + 1(也有从根节点向下计数的定义,需注意场景);
  • :一个节点拥有的子树数量(叶子节点的度为 0);
  • 叶子节点:度为 0 的节点(无任何子节点)。

1.2 二叉树的定义

二叉树是树的特殊形式,其递归定义为:

  • 空树:无任何节点;
  • 非空树:由根节点、左子树、右子树组成,且左子树和右子树均为二叉树,左右子树顺序严格区分,不可交换

⚠️ 注意:二叉树 ≠ 度为 2 的树。二叉树的节点度可以是 0、1、2,且即使只有一个子节点,也需明确是左子节点还是右子节点(如度为 1 的节点,子节点是右节点仍属于二叉树)。

1.3 二叉树节点的 JS 表示

在 JavaScript 中,二叉树节点有两种常见表示方式,核心包含三个属性:val(节点数据)、left(左子节点引用)、right(右子节点引用)。

// 方式1:构造函数定义节点(可复用创建节点)
class TreeNode {
  constructor(val) {
    this.val = val;
    this.left = this.right = null; // 初始左右子节点为空
  }
}

// 方式2:对象字面量表示整棵树(直观,适合快速定义示例树)
const root = {
  val: 'A',
  left: {
    val: 'B',
    left: { val: 'D', left: null, right: null },
    right: { val: 'E', left: null, right: null }
  },
  right: {
    val: 'C',
    left: null,
    right: { val: 'F', left: null, right: null }
  }
};

二、二叉树遍历的核心逻辑

遍历的目标是按规则访问每个节点,二叉树遍历分为两大类:

  • 深度优先遍历(DFS) :优先遍历子树(递归实现为主),包括前序、中序、后序 —— 三者核心区别是「根节点的访问时机」,左右子树的遍历顺序始终是 “先左后右”;
  • 广度优先遍历(BFS) :优先遍历层级(迭代实现),即层序遍历 —— 按 “从上到下、从左到右” 访问每一层节点,需借助队列实现。

三、深度优先遍历:递归实现前 / 中 / 后序

递归是实现 DFS 的天然方式,因为二叉树本身是递归定义的(每个子树都是二叉树)。递归的核心要素:

  1. 终止条件:节点为 null 时停止递归(空树无需访问);
  2. 子问题分解:将 “遍历整棵树” 分解为 “访问根节点 + 遍历左子树 + 遍历右子树”,仅调整根节点的访问顺序即可得到不同遍历方式。

以下均以示例树(根 A,左子树 B(左 D、右 E),右子树 C(右 F))为例实现。

3.1 前序遍历(根→左→右)

前序遍历规则:先访问根节点,再递归遍历左子树,最后递归遍历右子树。

// 示例树定义
const root = {
  val: 'A',
  left: {
    val: 'B',
    left: { val: 'D', left: null, right: null },
    right: { val: 'E', left: null, right: null }
  },
  right: {
    val: 'C',
    left: null,
    right: { val: 'F', left: null, right: null }
  }
};

// 前序遍历函数
function preorder(root) {
  // 终止条件:空节点直接返回
  if (!root) return;
  // 1. 访问根节点
  console.log(root.val);
  // 2. 递归遍历左子树
  preorder(root.left);
  // 3. 递归遍历右子树
  preorder(root.right);
}

// 执行遍历
preorder(root); // 输出顺序:A → B → D → E → C → F

逻辑解析:调用 preorder(root) 时,先打印根节点 A,再递归处理左子树 B;处理 B 时先打印 B,再递归处理 B 的左子树 D(打印 D,无左右子树,递归终止);回到 B 的逻辑,递归处理 B 的右子树 E(打印 E,递归终止);回到根节点 A 的逻辑,递归处理右子树 C(打印 C,再递归处理 C 的右子树 F,打印 F)。

3.2 中序遍历(左→根→右)

中序遍历规则:先递归遍历左子树,再访问根节点,最后递归遍历右子树。

// 中序遍历函数
function inorder(root) {
  if (!root) return;
  // 1. 递归遍历左子树
  inorder(root.left);
  // 2. 访问根节点
  console.log(root.val);
  // 3. 递归遍历右子树
  inorder(root.right);
}

inorder(root); // 输出顺序:D → B → E → A → C → F

逻辑解析:处理根节点 A 时,先递归左子树 B;处理 B 时先递归左子树 D(无左子树,打印 D),再打印 B,接着递归 B 的右子树 E(打印 E);回到 A 的逻辑,打印 A,再递归右子树 C(无左子树,打印 C,再递归 C 的右子树 F,打印 F)。

3.3 后序遍历(左→右→根)

后序遍历规则:先递归遍历左子树,再递归遍历右子树,最后访问根节点。

// 后序遍历函数
function postorder(root) {
  if (!root) return;
  // 1. 递归遍历左子树
  postorder(root.left);
  // 2. 递归遍历右子树
  postorder(root.right);
  // 3. 访问根节点
  console.log(root.val);
}

postorder(root); // 输出顺序:D → E → B → F → C → A

逻辑解析:处理根节点 A 时,先递归左子树 B;处理 B 时先递归左子树 D(打印 D),再递归右子树 E(打印 E),最后打印 B;回到 A 的逻辑,递归右子树 C(先递归左子树(空),再递归右子树 F(打印 F),最后打印 C);最后打印根节点 A。

四、广度优先遍历:迭代实现层序遍历

层序遍历(Level Order Traversal)按 “从上到下、每层从左到右” 访问节点,递归难以直接实现,需借助队列(先进先出 FIFO)的特性:

  1. 初始化队列,将根节点入队;
  2. 循环取出队头节点,访问该节点;
  3. 将该节点的左、右子节点依次入队(保证下一层按左到右顺序);
  4. 直到队列为空,遍历完成。

4.1 层序遍历代码实现

// 示例树(数值型,便于验证结果)
const root = {
  val: 1,
  left: {
    val: 2,
    left: { val: 4, left: null, right: null },
    right: { val: 5, left: null, right: null }
  },
  right: { val: 3, left: null, right: null }
};

// 层序遍历函数(返回遍历结果数组)
function levelOrder(root) {
  // 空树返回空数组
  if (!root) return [];
  // 结果数组:存储遍历结果
  const result = [];
  // 队列:初始存入根节点
  const queue = [root];

  // 迭代:队列不为空则继续
  while (queue.length > 0) {
    // 取出队头节点(shift() 是数组的出队操作)
    const currentNode = queue.shift();
    // 访问节点:存入结果数组
    result.push(currentNode.val);
    // 左子节点存在则入队
    if (currentNode.left) {
      queue.push(currentNode.left);
    }
    // 右子节点存在则入队
    if (currentNode.right) {
      queue.push(currentNode.right);
    }
  }

  return result;
}

// 执行并打印结果
console.log(levelOrder(root)); // 输出:[1, 2, 3, 4, 5]

4.2 层序遍历逻辑解析

  1. 初始化:queue = [root(1)]result = []
  2. 第一次循环:取出 1,result = [1],左子节点 2、右子节点 3 入队 → queue = [2, 3]
  3. 第二次循环:取出 2,result = [1,2],左子节点 4、右子节点 5 入队 → queue = [3,4,5]
  4. 第三次循环:取出 3,result = [1,2,3],无左右子节点 → queue = [4,5]
  5. 第四次循环:取出 4,result = [1,2,3,4],无左右子节点 → queue = [5]
  6. 第五次循环:取出 5,result = [1,2,3,4,5],无左右子节点 → queue = []
  7. 队列为空,循环终止,返回结果数组。

五、遍历的复杂度分析

时间复杂度

所有遍历方式的时间复杂度均为 O(n)(n 为节点数):每个节点仅被访问一次,无论是递归还是迭代,都需遍历全部节点。

空间复杂度

  • 递归遍历(前 / 中 / 后序):空间复杂度为 O(h)(h 为树的高度),递归调用栈的深度等于树的高度。最坏情况(斜树,所有节点只有左 / 右子节点)下 h=n,空间复杂度为 O(n);最优情况(满二叉树)下 h=logn,空间复杂度为 O(logn)
  • 层序遍历:空间复杂度为 O(n),队列最多存储一层的节点,满二叉树的最后一层节点数为 n/2,因此最坏空间复杂度为 O(n)

六、总结

二叉树的遍历是算法面试的高频考点,核心在于理解:

  1. 递归遍历的本质是 “分解子问题”,前 / 中 / 后序仅调整根节点的访问顺序,代码简洁但存在栈溢出风险(如深度极大的树);
  2. 层序遍历的核心是利用队列的 FIFO 特性实现层级访问,迭代方式更稳定但代码稍复杂;
  3. 掌握遍历后可延伸解决二叉树的其他问题(如求树的高度、判断对称二叉树、路径求和等),因为这些问题的核心都是 “按特定顺序访问节点”。

在实际开发中,DOM 树的遍历、组件树的渲染、数据库 B + 树的查询等场景,均依赖二叉树遍历的核心思想。理解遍历的底层逻辑,是掌握更复杂树结构算法的基础