1. 面试题:二叉树的前序、中序、后序遍历分别是什么?
核心思路(一句话)
"前、中、后"描述的是根节点访问时机:前序是"根左右",中序是"左根右",后序是"左右根"。
解决方案流程图
text
遍历二叉树
│
├── 前序:根 → 左 → 右
│
├── 中序:左 → 根 → 右
│
└── 后序:左 → 右 → 根
结构化逻辑思维
记忆时不要死记整句话,只需要抓住一个核心:
text
左、右的位置基本固定
↓
区别只有"根"什么时候访问
↓
前序:根在前
中序:根在中
后序:根在后
例如:
text
A
/ \
B C
/ \
D E
三种遍历结果:
text
前序:A B D E C
中序:D B E A C
后序:D E B C A
底层实现原理
递归实现非常直观:
text
前序:
visit(root)
traverse(root.left)
traverse(root.right)
中序:
traverse(root.left)
visit(root)
traverse(root.right)
后序:
traverse(root.left)
traverse(root.right)
visit(root)
本质就是改变 visit(root) 的位置。
完整示例代码
javascript
// 二叉树节点
class TreeNode {
constructor(value, left = null, right = null) {
this.value = value;
this.left = left;
this.right = right;
}
}
// 构造一棵二叉树
const root = new TreeNode(
'A',
new TreeNode(
'B',
new TreeNode('D'),
new TreeNode('E')
),
new TreeNode('C')
);
// 前序:根 -> 左 -> 右
function preorder(root, result = []) {
if (root === null) return result;
result.push(root.value); // 先访问根
preorder(root.left, result); // 再访问左子树
preorder(root.right, result); // 最后访问右子树
return result;
}
// 中序:左 -> 根 -> 右
function inorder(root, result = []) {
if (root === null) return result;
inorder(root.left, result); // 先访问左子树
result.push(root.value); // 再访问根
inorder(root.right, result); // 最后访问右子树
return result;
}
// 后序:左 -> 右 -> 根
function postorder(root, result = []) {
if (root === null) return result;
postorder(root.left, result); // 先访问左子树
postorder(root.right, result); // 再访问右子树
result.push(root.value); // 最后访问根
return result;
}
console.log(preorder(root)); // ["A", "B", "D", "E", "C"]
console.log(inorder(root)); // ["D", "B", "E", "A", "C"]
console.log(postorder(root)); // ["D", "E", "B", "C", "A"]
复杂度
对于包含 n 个节点的二叉树:
text
时间复杂度:O(n)
空间复杂度:
递归调用栈:O(h)
h = 树的高度
最坏情况下二叉树退化成链表:
text
h = n
空间复杂度 O(n)
如果是比较平衡的树:
text
h ≈ log n
空间复杂度 O(log n)
主要矛盾:遍历顺序不同。
次要矛盾:递归还是迭代,以及空间复杂度。
2. 面试题:二叉树的不同遍历方式分别有什么使用场景?
核心思路(一句话)
不是"哪种遍历更好",而是看你需要什么顺序:访问父节点优先用前序,获取有序结果常用中序,子节点处理完再处理父节点常用后序。
解决方案架构图
text
二叉树遍历
│
┌─────────────┼─────────────┐
↓ ↓ ↓
前序 中序 后序
根左右 左根右 左右根
│ │ │
↓ ↓ ↓
父节点优先处理 二叉搜索树排序 子节点先处理
│ │ │
↓ ↓ ↓
树结构复制/序列化 获取有序序列 表达式计算/
文件目录类比 第K小元素 删除/释放等
① 前序遍历:根 → 左 → 右
适合先处理父节点,再处理子节点的场景。
例如:
text
A
/ \
B C
前序:
text
A → B → C
一个典型场景是树结构的序列化、复制,以及各种"先处理当前节点,再处理子节点"的操作。
② 中序遍历:左 → 根 → 右
最经典的应用是:
二叉搜索树(Binary Search Tree)的中序遍历,可以得到升序排列的节点序列。
例如:
text
5
/ \
3 8
/ \
1 4
中序:
text
1 → 3 → 4 → 5 → 8
因此可以用于:
- 二叉搜索树排序
- 获取二叉搜索树中的第 K 小元素
- 验证一棵树是否满足二叉搜索树的有序性
注意:
text
中序遍历本身 ≠ 排序算法
只有当树满足二叉搜索树的有序性质时,中序遍历结果才天然有序。
③ 后序遍历:左 → 右 → 根
核心特点:
处理父节点之前,子节点已经处理完。
因此适合存在"依赖子节点结果"的场景。
例如:
text
+
/ \
2 3
后序:
text
2 → 3 → +
这恰好对应:
text
逆波兰表达式 / 后缀表达式
所以表达式计算就是一个非常典型的应用。
此外,后序也适合:
- 删除树
- 计算目录总大小
- 计算树高度
- 计算子树信息
- 编译器语法树处理
- 表达式求值
3. 面试题:为什么二叉搜索树的中序遍历能够得到有序数组?
核心思路(一句话)
因为二叉搜索树规定"左子树 < 根 < 右子树",而中序恰好按照"左 → 根 → 右"的顺序访问,所以自然得到升序结果。
架构图
text
二叉搜索树性质
根
/ \
更小 更大
↓ ↓
左子树 右子树
↓
中序:左 → 根 → 右
↓
从小到大
↓
有序数组
示例
javascript
class TreeNode {
constructor(value, left = null, right = null) {
this.value = value;
this.left = left;
this.right = right;
}
}
const root = new TreeNode(
5,
new TreeNode(
3,
new TreeNode(1),
new TreeNode(4)
),
new TreeNode(8)
);
function inorder(root, result = []) {
if (root === null) return result;
// 先访问所有更小的节点
inorder(root.left, result);
// 再访问当前节点
result.push(root.value);
// 最后访问所有更大的节点
inorder(root.right, result);
return result;
}
console.log(inorder(root));
// [1, 3, 4, 5, 8]
边界场景
需要注意:
text
普通二叉树
↓
中序遍历
↓
不一定有序
只有:
text
二叉搜索树
+
中序遍历
↓
有序序列
如果允许重复值,还需要明确具体的二叉搜索树定义,例如规定:
text
左子树 ≤ 根 < 右子树
或者:
text
左子树 < 根 ≤ 右子树
不同实现规则会影响重复值应该放在哪里。
4. 面试题:为什么表达式计算经常使用后序遍历?
核心思路(一句话)
后序遍历保证一个运算符出现时,它依赖的左右操作数已经准备好,因此非常适合表达式求值。
解决方案流程图
以:
text
(2 + 3) * 4
为例。
表达式树:
text
*
/ \
+ 4
/ \
2 3
后序遍历:
text
2 → 3 → + → 4 → *
也就是:
text
2 3 + 4 *
这就是后缀表达式 / 逆波兰表达式。
然后使用栈:
text
2 → 入栈
3 → 入栈
+ → 取出 2、3 → 计算 5 → 入栈
4 → 入栈
* → 取出 5、4 → 计算 20 → 入栈
最终:20
底层原理
后序遍历最大的价值不是"顺序特殊",而是:
text
操作数
↓
操作数
↓
运算符
↓
可以立即计算
因此可以使用栈实现。
完整示例:计算后缀表达式
javascript
function evaluatePostfix(expression) {
// 按空格拆分,例如:
// "2 3 + 4 *"
const tokens = expression.trim().split(/\s+/);
const stack = [];
for (const token of tokens) {
// 数字直接入栈
if (!Number.isNaN(Number(token))) {
stack.push(Number(token));
continue;
}
// 运算符需要取出右操作数和左操作数
// 注意顺序:先取出来的是右操作数
const right = stack.pop();
const left = stack.pop();
if (left === undefined || right === undefined) {
throw new Error('表达式不合法');
}
switch (token) {
case '+':
stack.push(left + right);
break;
case '-':
stack.push(left - right);
break;
case '*':
stack.push(left * right);
break;
case '/':
if (right === 0) {
throw new Error('不能除以 0');
}
stack.push(left / right);
break;
default:
throw new Error(`不支持的运算符:${token}`);
}
}
// 最终应该只剩一个结果
if (stack.length !== 1) {
throw new Error('表达式不合法');
}
return stack[0];
}
console.log(evaluatePostfix('2 3 + 4 *'));
// 20
如果是在线计算器怎么办?
不能简单地:
javascript
eval(userInput)
因为用户输入的是不可信字符串,直接执行会把它当成 JavaScript 代码。
更合理的流程是:
text
用户输入
↓
词法分析
↓
Token
↓
语法分析
↓
抽象语法树
↓
后序遍历 / 直接递归求值
↓
计算结果
如果只是实现一个简单的四则运算计算器,还可以使用:
text
中缀表达式
↓
调度场算法
↓
后缀表达式
↓
栈求值
一个简单计算器并不需要完整编译器,可以实现一个词法分析器 + 简单语法分析器 + 求值器即可。
5. 面试题:前序、中序、后序遍历的时间复杂度和空间复杂度是多少?
核心思路(一句话)
三种遍历都会访问每个节点一次,所以时间复杂度都是 O(n),差别主要在访问顺序,而不是时间复杂度。
text
n = 节点数量
时间复杂度:
前序 O(n)
中序 O(n)
后序 O(n)
递归空间:
text
O(h)
h = 二叉树高度
例如:
text
平衡树:
A
/ \
B C
/ \
D E
h ≈ log n
空间 O(log n)
极端退化:
text
A
\
B
\
C
\
D
h = n
空间 O(n)
如果改成显式栈迭代,也仍然主要是:
text
时间:O(n)
额外空间:O(h)
6. 面试题:如何快速判断一道二叉树遍历题?
核心思路(一句话)
先画树,再只盯住"根节点什么时候输出",不要一上来死算答案。
面试现场解题流程
text
第一步:画出二叉树
↓
第二步:确认题目要求
↓
第三步:判断"根"的位置
↓
┌───┼───┐
↓ ↓ ↓
前序 中序 后序
↓ ↓ ↓
根左右 左根右 左右根
↓
第四步:从根节点递归展开
↓
第五步:得到遍历结果
记忆口诀:
text
前序:根左右
中序:左根右
后序:左右根
真正需要理解的是:前、中、后只是在改变"根节点的访问时机"。
满分答案
面试题:说一下二叉树的前序、中序、后序遍历,以及它们的应用。
核心思路:
二叉树遍历的核心区别只有一个:根节点什么时候访问。
- 前序:根 → 左 → 右
- 中序:左 → 根 → 右
- 后序:左 → 右 → 根
可以记成:
text
前序:根左右
中序:左根右
后序:左右根
1. 底层原理
本质都是深度优先遍历,只是 visit(root) 的位置不同:
text
前序:
访问根 → 遍历左 → 遍历右
中序:
遍历左 → 访问根 → 遍历右
后序:
遍历左 → 遍历右 → 访问根
三种遍历都会访问每个节点一次,所以:
text
时间复杂度:O(n)
递归空间复杂度:O(h)
h 是树的高度
2. 主要应用
前序:父节点先处理。
适合树结构复制、序列化,以及"先处理当前节点,再处理子节点"的场景。
中序:左 → 根 → 右。
二叉搜索树满足:
text
左子树 < 根 < 右子树
所以对二叉搜索树进行中序遍历,可以得到升序结果,也可以用于获取第 K 小元素等。
后序:子节点先处理。
适合"必须先得到子节点结果,再处理父节点"的场景,例如:
text
表达式计算
树高度计算
目录大小统计
删除树节点
其中表达式:
text
*
/ \
+ 4
/ \
2 3
后序遍历:
text
2 → 3 → + → 4 → *
得到后缀表达式:
text
2 3 + 4 *
再结合栈就可以完成求值。
3. 边界和易错点
第一,中序遍历不等于排序,只有对满足二叉搜索树性质的树进行中序遍历,结果才是有序的。
第二,文件目录是普通多叉树,不是二叉树。如果采用"先访问目录,再递归访问子目录"的方式,本质上属于普通树的先序深度优先遍历。
第三,在线计算器不应该直接使用:
javascript
eval(userInput)
更合理的做法是:
text
用户输入
↓
词法分析
↓
语法分析
↓
表达式树 / 后缀表达式
↓
栈或树遍历求值
一句话总结:
前序是根左右,适合父节点先处理;中序是左根右,二叉搜索树中可以得到有序序列;后序是左右根,适合子节点结果先计算,再处理父节点。三者时间复杂度都是 O(n),真正的区别是节点访问顺序和由此产生的应用场景。