树是数据结构中核心的非线性结构,相较于数组、链表等线性结构,它能高效组织具有层级关系的数据(如文件系统、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 的天然方式,因为二叉树本身是递归定义的(每个子树都是二叉树)。递归的核心要素:
- 终止条件:节点为
null时停止递归(空树无需访问); - 子问题分解:将 “遍历整棵树” 分解为 “访问根节点 + 遍历左子树 + 遍历右子树”,仅调整根节点的访问顺序即可得到不同遍历方式。
以下均以示例树(根 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)的特性:
- 初始化队列,将根节点入队;
- 循环取出队头节点,访问该节点;
- 将该节点的左、右子节点依次入队(保证下一层按左到右顺序);
- 直到队列为空,遍历完成。
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 层序遍历逻辑解析
- 初始化:
queue = [root(1)],result = []; - 第一次循环:取出 1,
result = [1],左子节点 2、右子节点 3 入队 →queue = [2, 3]; - 第二次循环:取出 2,
result = [1,2],左子节点 4、右子节点 5 入队 →queue = [3,4,5]; - 第三次循环:取出 3,
result = [1,2,3],无左右子节点 →queue = [4,5]; - 第四次循环:取出 4,
result = [1,2,3,4],无左右子节点 →queue = [5]; - 第五次循环:取出 5,
result = [1,2,3,4,5],无左右子节点 →queue = []; - 队列为空,循环终止,返回结果数组。
五、遍历的复杂度分析
时间复杂度
所有遍历方式的时间复杂度均为 O(n)(n 为节点数):每个节点仅被访问一次,无论是递归还是迭代,都需遍历全部节点。
空间复杂度
- 递归遍历(前 / 中 / 后序):空间复杂度为
O(h)(h 为树的高度),递归调用栈的深度等于树的高度。最坏情况(斜树,所有节点只有左 / 右子节点)下h=n,空间复杂度为O(n);最优情况(满二叉树)下h=logn,空间复杂度为O(logn)。 - 层序遍历:空间复杂度为
O(n),队列最多存储一层的节点,满二叉树的最后一层节点数为n/2,因此最坏空间复杂度为O(n)。
六、总结
二叉树的遍历是算法面试的高频考点,核心在于理解:
- 递归遍历的本质是 “分解子问题”,前 / 中 / 后序仅调整根节点的访问顺序,代码简洁但存在栈溢出风险(如深度极大的树);
- 层序遍历的核心是利用队列的 FIFO 特性实现层级访问,迭代方式更稳定但代码稍复杂;
- 掌握遍历后可延伸解决二叉树的其他问题(如求树的高度、判断对称二叉树、路径求和等),因为这些问题的核心都是 “按特定顺序访问节点”。
在实际开发中,DOM 树的遍历、组件树的渲染、数据库 B + 树的查询等场景,均依赖二叉树遍历的核心思想。理解遍历的底层逻辑,是掌握更复杂树结构算法的基础