面试题:二叉树的遍历及使用场景

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),真正的区别是节点访问顺序和由此产生的应用场景。

相关推荐
a187927218319 天前
【算法】树(二):队列与祖先——BFS 骨架三配件、短路透传与合流
算法·leetcode·二叉树·树·bfs·算法讲解·树遍历
wabs66610 天前
关于二叉树【力扣572.另一棵树的子树的思考】
数据结构·c++·算法·leetcode·二叉树
wabs66611 天前
关于二叉树【力扣100.相同的树的思考】
数据结构·c++·算法·leetcode·二叉树·递归法
wabs66613 天前
关于二叉树【力扣101.对称二叉树的思考】
数据结构·c++·算法·leetcode·二叉树
-dzk-18 天前
【二叉树】LC 236.二叉树的最近公共祖先
数据结构·二叉树
wabs66619 天前
关于二叉树【力扣116.填充每个节点的下一个右侧节点指针的思考】
数据结构·c++·算法·leetcode·二叉树·层序遍历
洋不写bug20 天前
二叉树(四)经典算法题目解析,公共祖先,二叉树构造,非递归遍历
数据库·算法·二叉树·二叉树构造·公共祖先
wabs66621 天前
关于二叉树【429.N叉树的层序遍历的思考】
数据结构·c++·算法·leetcode·二叉树
wabs66623 天前
关于二叉树【力扣199.二叉树的右视图的思考】
数据结构·c++·算法·leetcode·二叉树·层序遍历