树遍历专题(DFS/BFS 入门)
树遍历专题(DFS/BFS 入门)
📖 学习路径: 01 树遍历 → 02 DFS/BFS 进阶 → 03 动态规划
一、核心概念
1. DFS vs BFS
| 特性 | DFS(深度优先) | BFS(广度优先) |
|---|---|---|
| 数据结构 | 栈(递归调用栈 / 显式栈) | 队列 |
| 遍历方式 | 一条路走到底,再回溯 | 一层一层向外扩展 |
| 空间 | O(h),h 为树高 | O(w),w 为最大宽度 |
| 时间 | O(n) | O(n) |
2. 快速决策
需要最短路径/层级信息?
├── 是 → BFS(层序遍历、最小深度、右视图)
└── 否 → 需要记录完整路径?
├── 是 → DFS + 回溯(路径总和 II)
└── 否 → DFS 递归(最大深度、路径总和、平衡树)
3. 本专题统一代码风格
约定: 以下所有代码使用相同的写法模式,方便对照理解。
- 类型注解完整(
TreeNode[]、number[][]等)- 条件判断用
while (queue.length > 0)而非while (queue.length)- 结果变量统一命名为
result- DFS 内部函数统一命名为
dfs- 队列/栈变量统一命名:
queue/stack
二、代码框架(本专题通用模板)
框架一:DFS 递归(树遍历核心模板)
const dfsRecursive = (root: TreeNode | null): number[] => {
const result: number[] = [];
const dfs = (node: TreeNode | null) => {
if (!node) return; // ① 终止条件:空节点
// ② 处理当前节点(位置决定遍历顺序)
dfs(node.left); // ③ 递归左子树
dfs(node.right); // ④ 递归右子树
};
dfs(root);
return result;
};
💡 改变
result.push(node.val)的位置 = 前序/中序/后序
框架二:BFS 层序遍历(按层处理)
const bfsLevelOrder = (root: TreeNode | null): number[][] => {
if (!root) return [];
const result: number[][] = [];
const queue: TreeNode[] = [root]; // ① 初始化队列
while (queue.length > 0) { // ② 队列不空就继续
const levelSize = queue.length; // ③ 记录当前层大小
const level: number[] = [];
for (let i = 0; i < levelSize; i++) { // ④ 处理当前层
const node = queue.shift()!;
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(level); // ⑤ 收集当前层结果
}
return result;
};
三、典型例题(由简到难)
🟢 基础遍历
1. 二叉树的前/中/后序遍历(LeetCode 144/94/145)
问题: 给定二叉树的根节点,按前序(根→左→右)、中序(左→根→右)、后序(左→右→根)返回遍历结果。
思路: 三种遍历的区别仅在于 result.push(node.val) 的位置。
前序:访问 → 左递归 → 右递归 【根左右】
中序:左递归 → 访问 → 右递归 【左根右】
后序:左递归 → 右递归 → 访问 【左右根】
递归版(统一模板):
// ========== 前序:根 → 左 → 右 ==========
const preorderRecursive = (root: TreeNode | null): number[] => {
const result: number[] = [];
const dfs = (node: TreeNode | null) => {
if (!node) return;
result.push(node.val); // ← 访问在递归前
dfs(node.left);
dfs(node.right);
};
dfs(root);
return result;
};
// ========== 中序:左 → 根 → 右 ==========
const inorderRecursive = (root: TreeNode | null): number[] => {
const result: number[] = [];
const dfs = (node: TreeNode | null) => {
if (!node) return;
dfs(node.left);
result.push(node.val); // ← 访问在左右递归之间
dfs(node.right);
};
dfs(root);
return result;
};
// ========== 后序:左 → 右 → 根 ==========
const postorderRecursive = (root: TreeNode | null): number[] => {
const result: number[] = [];
const dfs = (node: TreeNode | null) => {
if (!node) return;
dfs(node.left);
dfs(node.right);
result.push(node.val); // ← 访问在递归后
};
dfs(root);
return result;
};
迭代版(显式栈):
// ========== 前序迭代:栈 → 弹出访问 → 先压右再压左 ==========
const preorderIterative = (root: TreeNode | null): number[] => {
if (!root) return [];
const result: number[] = [];
const stack: TreeNode[] = [root];
while (stack.length > 0) {
const node = stack.pop()!;
result.push(node.val);
if (node.right) stack.push(node.right); // 先压右
if (node.left) stack.push(node.left); // 后压左(先出栈)
}
return result;
};
// ========== 中序迭代:一路向左 → 弹出访问 → 转向右 ==========
const inorderIterative = (root: TreeNode | null): number[] => {
const result: number[] = [];
const stack: TreeNode[] = [];
let cur = root;
while (cur || stack.length > 0) {
while (cur) { // 一路向左压栈
stack.push(cur);
cur = cur.left;
}
cur = stack.pop()!; // 弹出访问
result.push(cur.val);
cur = cur.right; // 转向右子树
}
return result;
};
// ========== 后序迭代:前序"根右左"的反转 = "左右根" ==========
const postorderIterative = (root: TreeNode | null): number[] => {
if (!root) return [];
const result: number[] = [];
const stack: TreeNode[] = [root];
while (stack.length > 0) {
const node = stack.pop()!;
result.push(node.val);
if (node.left) stack.push(node.left); // 先压左
if (node.right) stack.push(node.right); // 后压右
}
return result.reverse(); // 反转即得后序
};
记忆技巧:
| 遍历 | 递归写法 | 迭代技巧 |
|---|---|---|
| 前序 | push → left → right | 栈,先压右再压左 |
| 中序 | left → push → right | 一路向左,弹出访问 |
| 后序 | left → right → push | 前序“根右左”的反转 |
2. 二叉树的层序遍历(LeetCode 102)
问题: 按从上到下、从左到右的顺序,逐层返回节点值。
思路: BFS 模板直接套用,levelSize 控制每层处理数量。
const levelOrder = (root: TreeNode | null): number[][] => {
if (!root) return [];
const result: number[][] = [];
const queue: TreeNode[] = [root];
while (queue.length > 0) {
const levelSize = queue.length; // ① 当前层的节点数
const level: number[] = [];
for (let i = 0; i < levelSize; i++) { // ② 只处理当前层
const node = queue.shift()!;
level.push(node.val);
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
result.push(level); // ③ 收集当前层
}
return result;
};
执行过程:
3
/ \
9 20
/ \
15 7
第1轮:levelSize=1, 处理[3], 入队[9,20] → result=[[3]]
第2轮:levelSize=2, 处理[9,20], 入队[15,7] → result=[[3],[9,20]]
第3轮:levelSize=2, 处理[15,7], 入队[] → result=[[3],[9,20],[15,7]]
🟡 深度/高度相关
3. 二叉树的最大深度(LeetCode 104)
问题: 求二叉树的最大深度(根节点到最远叶子节点的路径上的节点数)。
思路:
- DFS:当前深度 = max(左子树深度, 右子树深度) + 1
- BFS:每处理一层,深度 +1
// ====== DFS 版:自底向上 ======
const maxDepthDFS = (root: TreeNode | null): number => {
if (!root) return 0;
return Math.max(maxDepthDFS(root.left), maxDepthDFS(root.right)) + 1;
};
// ====== BFS 版:自顶向下(按层计数) ======
const maxDepthBFS = (root: TreeNode | null): number => {
if (!root) return 0;
const queue: TreeNode[] = [root];
let depth = 0;
while (queue.length > 0) {
const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) {
const node = queue.shift()!;
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
depth++; // 每处理完一层,深度 +1
}
return depth;
};
执行过程(DFS 版):
3
/ \
9 20
/ \
15 7
maxDepth(3)
→ max( maxDepth(9), maxDepth(20) ) + 1
→ max( 1, max(maxDepth(15), maxDepth(7)) + 1 ) + 1
→ max( 1, max(1, 1) + 1 ) + 1
→ max( 1, 2 ) + 1
→ 3
4. 二叉树的最小深度(LeetCode 111)
问题: 求二叉树的最小深度(根节点到最近叶子节点的路径上的节点数)。
思路: BFS 天然优势——第一个遇到的叶子节点所在层就是最小深度。
const minDepth = (root: TreeNode | null): number => {
if (!root) return 0;
const queue: TreeNode[] = [root];
let depth = 0;
while (queue.length > 0) {
const levelSize = queue.length;
depth++;
for (let i = 0; i < levelSize; i++) {
const node = queue.shift()!;
// 叶子节点:左右都为空,直接返回当前深度
if (!node.left && !node.right) return depth;
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return depth;
};
💡 BFS 天然适合求最小深度:第一个叶子节点所在的深度就是答案。DFS 也可以做,但需要额外处理“只有单侧子树”的情况。
5. 平衡二叉树(LeetCode 110)
问题: 判断二叉树是否是平衡二叉树(任意节点的左右子树高度差不超过 1)。
思路: 自底向上递归,用 -1 标记不平衡状态,避免重复计算高度。
const isBalanced = (root: TreeNode | null): boolean => {
// 返回高度,如果不平衡则返回 -1
const getHeight = (node: TreeNode | null): number => {
if (!node) return 0;
const leftH = getHeight(node.left);
const rightH = getHeight(node.right);
// 子树不平衡,或当前节点高度差 > 1 → 返回 -1
if (leftH === -1 || rightH === -1 || Math.abs(leftH - rightH) > 1) {
return -1;
}
return Math.max(leftH, rightH) + 1;
};
return getHeight(root) !== -1;
};
执行过程:
1
/ \
2 2 ← 节点2:左右高度差=0,返回1
/ \ / \
3 3 3 3 ← 节点3:叶子,返回1
\
4 ← 节点4:叶子,返回1
getHeight(3-最右): left=0, right=1, |0-1|=1≤1 → 返回2
getHeight(2-右): left=2, right=2, |2-2|=0≤1 → 返回3
getHeight(1): left=3, right=3, |3-3|=0≤1 → 返回4
isBalanced = true ✅
🟠 路径/视图相关
6. 路径总和(LeetCode 112)
问题: 判断二叉树中是否存在根→叶路径,其节点值之和等于
targetSum。
思路: DFS 递归,每深入一层减去当前节点值,到叶子时检查剩余值是否为 0。
const hasPathSum = (root: TreeNode | null, targetSum: number): boolean => {
if (!root) return false;
// 到达叶子节点:检查当前值是否等于剩余目标值
if (!root.left && !root.right) {
return root.val === targetSum;
}
// 递归检查左右子树,目标值减去当前节点值
return hasPathSum(root.left, targetSum - root.val)
|| hasPathSum(root.right, targetSum - root.val);
};
执行过程(targetSum = 22):
5
/ \
4 8
/ / \
11 13 4
/ \ \
7 2 1
hasPathSum(5, 22)
→ hasPathSum(4, 17) ← 22-5=17
→ hasPathSum(11, 13) ← 17-4=13
→ hasPathSum(7, 2) ← 13-11=2 → 7≠2 → false
→ hasPathSum(2, 2) ← 2==2 → true ✅
7. 路径总和 II(LeetCode 113)
问题: 找出所有根→叶路径,使得路径上节点值之和等于
targetSum,返回具体路径。
思路: DFS + 回溯。因为 path 数组是引用类型,多分支共享,必须 push → 递归 → pop。
const pathSum = (root: TreeNode | null, targetSum: number): number[][] => {
const result: number[][] = [];
const path: number[] = [];
const dfs = (node: TreeNode | null, remaining: number) => {
if (!node) return;
path.push(node.val); // ① 选择当前节点
// ② 叶子节点 + 值匹配 → 收集路径(深拷贝)
if (!node.left && !node.right && remaining === node.val) {
result.push([...path]);
}
dfs(node.left, remaining - node.val); // ③ 递归子节点
dfs(node.right, remaining - node.val);
path.pop(); // ④ 回溯:撤销当前选择,回到上一层的状态
};
dfs(root, targetSum);
return result;
};
为什么需要回溯? 因为 path 是引用类型,递归调用中所有分支共享同一个数组。
5
/ \
4 8
/ / \
11 13 4
targetSum=22 的调用过程:
path=[5]
→ path=[5,4]
→ path=[5,4,11]
→ path=[5,4,11,7] → 7≠11 → pop(7)
→ path=[5,4,11,2] → 2==11 → 收集 [5,4,11,2] ✅ → pop(2)
→ pop(11) ← 回到 [5,4]
→ pop(4) ← 回到 [5]
→ path=[5,8]
→ path=[5,8,13] → 13≠9 → pop(13)
→ path=[5,8,4] → 需要继续...
8. 二叉树的右视图(LeetCode 199)
问题: 从二叉树的右侧看,返回能看到的节点值(每层最右边的节点)。
思路: BFS 层序遍历,每层的最后一个节点就是右视图看到的节点。
const rightSideView = (root: TreeNode | null): number[] => {
if (!root) return [];
const result: number[] = [];
const queue: TreeNode[] = [root];
while (queue.length > 0) {
const levelSize = queue.length;
for (let i = 0; i < levelSize; i++) {
const node = queue.shift()!;
// 每层最后一个节点 = 右视图看到的节点
if (i === levelSize - 1) {
result.push(node.val);
}
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return result;
};
执行过程:
1 ← 看到 1
/ \
2 3 ← 看到 3
\ \
5 4 ← 看到 4
/
6 ← 看到 6
result = [1, 3, 4, 6]
🔴 多叉树扩展
9. N 叉树的前/后序遍历(LeetCode 589/590)
问题: 给定 N 叉树的根节点,返回前序/后序遍历结果。
与二叉树的区别: 二叉树用 node.left / node.right,N 叉树用 for (child of node.children)。
节点定义:
class Node {
val: number;
children: Node[];
constructor(val: number) {
this.val = val;
this.children = [];
}
}
前序和后序:
// ========== 前序:先访问根,再遍历子节点 ==========
const preorderNary = (root: Node | null): number[] => {
const result: number[] = [];
const dfs = (node: Node | null) => {
if (!node) return;
result.push(node.val); // 访问当前
for (const child of node.children) { // 遍历子节点
dfs(child);
}
};
dfs(root);
return result;
};
// ========== 后序:先遍历子节点,再访问根 ==========
const postorderNary = (root: Node | null): number[] => {
const result: number[] = [];
const dfs = (node: Node | null) => {
if (!node) return;
for (const child of node.children) { // 先遍历子节点
dfs(child);
}
result.push(node.val); // 再访问当前
};
dfs(root);
return result;
};
10. N 叉树的最大深度(LeetCode 559)
问题: 求 N 叉树的最大深度。
思路: 当前深度 = max(所有子节点的深度) + 1。
const maxDepthNary = (root: Node | null): number => {
if (!root) return 0;
let maxChildDepth = 0;
for (const child of root.children) {
maxChildDepth = Math.max(maxChildDepth, maxDepthNary(child));
}
return maxChildDepth + 1;
};
二叉树 vs N叉树 最大深度对比:
// 二叉树:直接取左右子树
return Math.max(dfs(root.left), dfs(root.right)) + 1;
// N叉树:遍历所有子节点取最大
let max = 0;
for (const child of root.children) {
max = Math.max(max, dfs(child));
}
return max + 1;
🟣 对称/结构相关
11. 对称二叉树(LeetCode 101)
问题: 判断二叉树是否关于中心轴对称。
思路: 双指针递归——同时比较左子树的左节点与右子树的右节点、左子树的右节点与右子树的左节点。
const isSymmetric = (root: TreeNode | null): boolean => {
if (!root) return true;
// 比较两棵子树是否镜像对称
const isMirror = (p: TreeNode | null, q: TreeNode | null): boolean => {
if (!p && !q) return true; // 都为空 → 对称
if (!p || !q) return false; // 一个为空 → 不对称
if (p.val !== q.val) return false; // 值不同 → 不对称
// 递归比较:p的左 vs q的右 AND p的右 vs q的左
return isMirror(p.left, q.right) && isMirror(p.right, q.left);
};
return isMirror(root.left, root.right);
};
执行过程:
1
/ \
2 2 ← isMirror(2, 2): 值相同
/ \ / \
3 4 4 3 ← isMirror(3,3): 值相同 ✅
isMirror(4,4): 值相同 ✅
✅ 对称
1
/ \
2 2 ← isMirror(2, 2): 值相同
\ \
3 3 ← isMirror(3, null): 一个空 → ❌ 不对称
四、本专题总结
题目速查表
| # | 题目 | 方法 | 关键技巧 |
|---|---|---|---|
| 1 | 前/中/后序遍历 | DFS递归 + 迭代栈 | push 位置决定遍历顺序 |
| 2 | 层序遍历 | BFS队列 | levelSize 按层控制 |
| 3 | 最大深度 | DFS/BFS | DFS: max(左,右)+1;BFS: 每层 depth++ |
| 4 | 最小深度 | BFS | 首次遇到叶子即返回 |
| 5 | 平衡二叉树 | DFS递归 | -1 标记不平衡,自底向上 |
| 6 | 路径总和 | DFS递归 | 减法传递剩余值 |
| 7 | 路径总和 II | DFS + 回溯 | push → 递归 → pop |
| 8 | 右视图 | BFS | i === levelSize - 1 |
| 9 | N叉树遍历 | DFS递归 | for (child of children) |
| 10 | N叉树最大深度 | DFS递归 | 遍历子节点取最大 |
| 11 | 对称二叉树 | DFS双指针 | isMirror(左.左, 右.右) && isMirror(左.右, 右.左) |
统一代码模板回顾
| 场景 | 模板 |
|---|---|
| 树遍历(需全部结果) | DFS 递归:dfs(node.left); dfs(node.right) |
| 按层处理 | BFS 队列 + levelSize |
| 求最短/最小 | BFS:首次满足条件即返回 |
| 记录路径 | DFS + 回溯:push → 递归 → pop |
| 自底向上求值 | DFS:return f(左) 运算 f(右) + 1 |
记忆口诀
- 求最短,用 BFS
- 找路径,用 DFS
- 要分层,BFS 稳
- N叉树,循环子
- 回溯记路径,push 再 pop
- 自底向上,递归返回值
关联题库
以下题目与本文知识点相关,可以跳转到题库练习: