作为前端开发者,你可能在面试时被问过「二叉树遍历」,也可能在刷LeetCode时被各种遍历题难住。但你有没有想过:为什么这道题能成为高频考点?其实答案很简单——二叉树的遍历是理解树结构、递归思想、甚至后端数据结构的基础。今天咱们就用最接地气的方式,一次性搞定所有遍历方式。
一、为什么先学二叉树?它到底长什么样?
要理解二叉树,先想象一个公司的组织架构:老板(根节点)下面有两个直属部门(左子树、右子树),每个部门又有自己的下属(子节点)。这种「一个节点最多有两个子节点」的结构,就是二叉树的核心特征。
举个具体的例子,假设有一个二叉树结构如下:
A
/ \
B C
/ \ \
D E F
这里的每个字母都是一个节点,A
是根节点,B
是左子节点,C
是右子节点,依此类推。这棵树的结构完美符合二叉树的定义:每个节点最多有两个子节点,且左右子树严格区分(比如B
的左子节点是D
,右是E
,顺序不能调换)。
二、遍历的本质:给节点「打卡」的顺序
所谓「遍历」,就是按照某种规则访问树中的每个节点一次。就像你去游乐园玩,需要按路线打卡每个项目。二叉树的遍历主要分两大类:
- 深度优先遍历(DFS):一条路走到底,再回头(先序、中序、后序)
- 广度优先遍历(BFS):按层访问,逐层推进(层序遍历)
咱们先从最常用的深度优先遍历开始。
(一)深度优先三兄弟:先序、中序、后序
这三个遍历方式的名字里藏着关键信息——根节点的访问顺序。
1. 先序遍历:根→左→右(先打根节点卡)
先序遍历的规则是:先访问根节点,再递归遍历左子树,最后递归遍历右子树。用上面的例子,遍历顺序应该是:
- 第一步:访问根节点
A
- 第二步:递归左子树
B
(此时B
成为当前子树的根)- 访问
B
,然后递归B
的左子树D
(D
是叶子节点,无左右子树,直接访问D
) - 回到
B
的右子树E
,访问E
- 访问
- 第三步:递归右子树
C
(C
是当前子树的根)- 访问
C
,然后递归C
的右子树F
(访问F
)
- 访问
最终顺序是:A → B → D → E → C → F
看看代码怎么实现:
function preorder(root) {
// 退出条件:当前节点为空时返回
if (!root) return;
// 先访问当前节点(根)
console.log(root.val);
// 递归左子树
preorder(root.left);
// 递归右子树
preorder(root.right);
}
代码的核心逻辑完美对应了「根→左→右」的顺序:先处理当前节点,再处理左,最后处理右。
2. 中序遍历:左→根→右(中间打根节点卡)
中序遍历的规则是:先递归遍历左子树,再访问根节点,最后递归遍历右子树。还是用上面的例子:
- 第一步:递归左子树
B
(此时B
是当前子树的根)- 递归
B
的左子树D
(D
无左子树,访问D
) - 访问
B
(此时B
的左子树已处理完) - 递归
B
的右子树E
(访问E
)
- 递归
- 第二步:访问根节点
A
(此时左子树已处理完) - 第三步:递归右子树
C
(C
是当前子树的根)C
无左子树,直接访问C
- 递归
C
的右子树F
(访问F
)
最终顺序是:D → B → E → A → C → F
对应的代码实现:
function inorder(root) {
if (!root) return;
// 先递归左子树
inorder(root.left);
// 中间访问当前节点(根)
console.log(root.val);
// 最后递归右子树
inorder(root.right);
}
这里的关键是把「访问当前节点」的操作放在了递归左子树之后、递归右子树之前,完美体现「左→根→右」。
3. 后序遍历:左→右→根(最后打根节点卡)
后序遍历的规则是:先递归遍历左子树,再递归遍历右子树,最后访问根节点。继续用例子验证:
- 第一步:递归左子树
B
- 递归
B
的左子树D
(访问D
) - 递归
B
的右子树E
(访问E
) - 访问
B
(此时B
的左右子树都处理完)
- 递归
- 第二步:递归右子树
C
- 递归
C
的右子树F
(访问F
) - 访问
C
(此时C
的左右子树处理完)
- 递归
- 第三步:访问根节点
A
(此时左右子树都处理完)
最终顺序是:D → E → B → F → C → A
代码实现:
function postorder(root) {
if (!root) return;
// 先递归左子树
postorder(root.left);
// 再递归右子树
postorder(root.right);
// 最后访问当前节点(根)
console.log(root.val);
}
这里「访问当前节点」的操作被放在了最后,严格遵循「左→右→根」。
三兄弟对比表
为了帮你快速区分,我做了个对比表:
(二)广度优先:层序遍历(按层打卡)
深度优先的三个兄弟是「一条路走到底」,而层序遍历则是「按层访问」。想象一下你站在树的顶端,从上到下、从左到右依次扫描每一层的节点。
用前面的例子,层序遍历的顺序是:
- 第一层:
A
- 第二层:
B
→C
- 第三层:
D
→E
→F
最终顺序是:A → B → C → D → E → F
层序遍历的实现需要借助「队列」(FIFO,先进先出)。具体步骤如下:
- 将根节点入队
- 循环取出队首节点,访问它
- 将该节点的左子节点和右子节点(如果存在)依次入队
- 重复步骤2-3,直到队列为空
代码实现:
function levelOrderTraversal(root) {
if (!root) return [];
const result = [];
const queue = [root]; // 初始化队列,根节点入队
while (queue.length > 0) {
const currentNode = queue.shift(); // 取出队首节点
result.push(currentNode.val);
// 左子节点入队
if (currentNode.left) queue.push(currentNode.left);
// 右子节点入队
if (currentNode.right) queue.push(currentNode.right);
}
return result;
}
这里的关键是用队列维护「当前层的节点」,每次处理完一个节点就把它的子节点加入队列,确保下一层节点能被顺序处理。
三、为什么要学这么多遍历方式?它们的实际用途
你可能会问:「学这么多遍历方式,实际开发中用得到吗?」答案是:不仅用得到,而且场景各不相同。
- 先序遍历:适合「从上到下」的操作,比如拷贝整棵树的结构(先创建根节点,再创建左子树,最后右子树)、解析JSON(先处理根字段,再处理子字段)。
- 中序遍历:二叉搜索树(BST)的中序遍历结果是有序的,这在排序和查找中非常有用。
- 后序遍历:适合「从下到上」的操作,比如计算文件大小(先计算所有子文件大小,最后汇总父文件夹)、垃圾回收(先回收子节点内存,再回收父节点)。
- 层序遍历:适合按层处理的场景,比如获取二叉树的最大宽度、判断是否为完全二叉树。
四、学习心得:如何避免混淆?
刚开始学的时候,我也经常把先序、中序、后序搞混。后来总结了两个方法:
1. 画图+手动模拟
遇到复杂的树结构时,先画出树的形状,然后用不同颜色的笔标注访问顺序。比如先序用红色,中序用蓝色,后序用绿色,直观看到区别。
2. 记住「递归的本质是栈」
递归的过程其实隐式维护了一个调用栈。以先序遍历为例,代码执行时会先处理根节点,然后把左子树压入栈顶,处理完左子树再处理右子树——这和栈「后进先出」的特性一致。理解了这一点,就能更清晰地把握递归的执行顺序。
五、总结
二叉树的遍历是前端工程师的基础技能,更是打开算法世界的一把钥匙。无论是面试中的高频题,还是实际开发中的场景(如虚拟DOM的遍历、树形结构的操作),都需要扎实的遍历基础。
最后再帮你总结一遍:
- 深度优先三兄弟:先序(根左右)、中序(左根右)、后序(左右根),用递归实现。
- 广度优先:层序(按层访问),用队列实现。
- 实际应用:根据业务需求选择合适的遍历方式。
下次遇到二叉树的题,不妨先画个图,手动模拟一遍遍历过程——你会发现,问题往往迎刃而解。