Skip to content

二叉树遍历算法

二叉树算法是整个算法大厦的基石。二叉树可以拓展为多叉树,而多叉树的遍历正是 BFS 与 DFS 的基础;树的遍历加上环判断,即可升级为图的遍历。学会二叉树遍历,就掌握了所有数据结构的遍历法则。

前置知识

DFS 遍历框架

本章聚焦树上的 DFS。网格/图上的 DFS(Flood Fill、连通分量等)见深度优先搜索专题

二叉树 DFS 分为前中后序三种,划分依据只有一条:在什么时机处理当前节点的数据

前序在进入左右子节点之前处理;中序在遍历完左子树、准备遍历右子树之前处理;后序在左右子树都遍历完成后处理。

ts
function dfs(node: TreeNode | null) {
  if (!node) return;
  // 前序位置(进入节点时)
  dfs(node.left);
  // 中序位置(左子树处理完,右子树处理前)
  dfs(node.right);
  // 后序位置(离开节点时)
}

若希望写成一个空间复杂度更低的非递归模板,可以直接套下面这个统一框架。和递归一样,只要在对应位置写逻辑即可:

ts
function dfs(root: TreeNode) {
  const stack: TreeNode[] = [];
  let node: TreeNode = root;

  // 只有后序遍历时需要额外记录上一次访问的节点
  let prev: TreeNode | null = null;

  while (node || stack.length) {
    // 先压栈所有左子树
    while (node) {
      // 前序:如果是前序遍历,在这里处理 node
      // doPre(node);
      stack.push(node);
      node = node.left;
    }

    // 1. 非后序的场景:直接转向右子树
    /**
     * const cur = stack.pop();
     * 中序:左子树已经全部处理完了,在这里处理当前节点
     * doIn(cur);
     * 
     * node = cur.right;
     */

    // 2. 后序的场景
    const peek = stack[stack.length - 1];
    // 右子树未处理过则先处理右子树
    if (peek.right && prev !== peek.right) {
      node = peek.right;
    } else {
      const cur = stack.pop();
      // 后序:右子树已经在刚才处理过了,才能处理当前节点
      // doPost(cur);
      prev = cur;
    }
  }
}

只写 doPre 就是前序,只写 doIn 就是中序,只写 doPost 就是后序;三者也可以组合使用。

BFS 遍历框架

本章聚焦树上的 BFS(层序遍历)。BFS 求最短路径、多源 BFS、抽象状态空间搜索等应用见广度优先搜索专题

BFS(层序遍历)的核心是队列:每次弹出一层节点,再把下一层节点入队。

ts
function bfs(root: TreeNode | null): number[][] {
  if (!root) return [];

  const res: number[][] = [];
  const queue: TreeNode[] = [root];

  while (queue.length) {
    const level: number[] = [];
    // 固定住当前层的节点数量,避免和下一层混在一起
    const len = queue.length;

    while(len--) {
      const node = queue.shift()!;
      level.push(node.val);

      if (node.left) queue.push(node.left);
      if (node.right) queue.push(node.right);
    }

    res.push(level);
  }

  return res;
}

如果题目是最短步数/最小深度这类最短路径问题,可以直接把 res 换成 depth 计数器:每处理完一层,depth++;首次命中目标就立刻返回。

前序、中序、后序的本质区别

理解三种遍历方式的信息流方向,是选择正确解法的关键:

遍历位置可获取的信息信息流方向适用场景
前序仅父节点传递下来的参数自顶向下(Top-Down)传递状态、路径记录
中序左子树已处理完毕左 → 根 → 右BST 有序操作
后序左右子树的完整结果(通过返回值)自底向上(Bottom-Up)需要子树信息的计算

核心规律:前序位置的代码只能从函数参数中获取父节点传递来的数据;而后序位置的代码不仅可以获取参数数据,还可以获取子树通过函数返回值传递回来的数据。

例如在 [257] 二叉树的所有路径 中,我们需要把从根到叶的路径通过参数一路往下传递,这就是典型的前序位置(Top-Down)处理。而在 [110] 平衡二叉树 中,我们需要先知道左右子树各自的高度,才能判断当前节点是否平衡,这就只能在后序位置(Bottom-Up)完成。

两种思维模式

掌握了前中后序的信息流方向后,还需要了解二叉树的两种底层解题思维,它们贯穿后续所有题型:

  • 遍历思维:用 traverse 函数遍历整棵树,配合外部变量累积结果,不需要返回值。
  • 分解问题思维:用有返回值的递归函数,通过子问题(子树)的答案推导原问题的答案。

这两种思维不是题型分类,而是编码手段——同一道题往往两种思维都能解,区别在于哪种写起来更自然。后续每个题型中会标注其更适合的思维模式。

面对一道二叉树题目时,按以下顺序思考:

  1. 单独抽出一个节点,它需要做什么?
  2. 它需要在什么时机(前序 / 中序 / 后序位置)做?
  3. 遍历思维还是分解问题思维更自然?

解题决策树

有了前置知识后,拿到一道二叉树题目,按以下决策树选择解法。前四类是基础遍历的直接应用,后三类是在此基础上的组合与进阶:

拿到一道二叉树题目

│  ── 基础遍历应用 ──

├─ 需要从根向下传递状态/路径?
│   └─ 是 → 前序遍历 .......................... → 见「一」

├─ 是 BST 且涉及有序性?
│   └─ 是 → 中序遍历 .......................... → 见「二」

├─ 需要先拿到子树的计算结果,再推导当前节点?
│   └─ 是 → 后序遍历 .......................... → 见「三」

├─ 涉及逐层处理/层级/最短路径?
│   └─ 是 → BFS 层序遍历 ...................... → 见「四」

│  ── 组合与进阶 ──

├─ 需要构造一棵树?
│   └─ 是 → 分解问题思维(按区间建根) .......... → 见「五」

├─ 需要对子树做签名比较/重复检测?
│   └─ 是 → 后序遍历 + 序列化签名 .............. → 见「六」

└─ 需要找最近公共祖先?
    └─ 是 → 后序遍历 + 分解问题 ................ → 见「七」

一、前序遍历:自顶向下传递状态

判断关键词:根到叶、路径记录、状态下传、回溯撤销。

适用场景:需要从根节点出发,把状态/路径一路往下传递。

思维模式:遍历思维为主——在前序位置做选择,往下递归,到达叶节点时判断或收集结果。

1.1 根到叶路径

例如在 [112] 路径总和 中,从根节点出发,在前序位置将目标值减去当前节点值并传递给子节点,到达叶节点时判断剩余值是否为 0。

ts
function traverse(node: TreeNode | null, path: number[], remain: number) {
  if (!node) return;

  // 关键1:前序做选择
  path.push(node.val);
  remain -= node.val;

  // 关键2:叶节点做判定/收集
  if (!node.left && !node.right && remain === 0) res.push([...path]);

  traverse(node.left, path, remain);
  traverse(node.right, path, remain);

  // 关键3:回溯撤销
  path.pop();
}

题型参考(框架微调):

题目在前序框架上的微调点
[112] 路径总和参数中携带剩余目标值,到叶节点判断是否为 0。
[113] 路径总和 II112 基础上增加 path 数组,命中后拷贝路径入结果。
[257] 二叉树的所有路径仍是路径下传,只是叶节点时把路径转成字符串。
[129] 求根节点到叶节点数字之和路径状态从数组改成数字累积:num = num * 10 + node.val
[437] 路径总和 III单纯前序不够,通常改成“每个节点作为起点 + DFS”或前缀和。

1.2 结构判断与修改

翻转、展平、判断对称等操作,两种思维都可以使用。

例如 [226] 翻转二叉树,每个节点要做的事就是交换左右子节点,放在前序位置(先交换再递归)或后序位置(先递归再组装)都能正确完成。

ts
// 遍历思维:前序位置直接修改
function invertTree(node: TreeNode | null): TreeNode | null {
  if (!node) return null;
  [node.left, node.right] = [node.right, node.left];
  invertTree(node.left);
  invertTree(node.right);
  return node;
}

后序写法也等价:先递归拿到 left/right,再在返回前交换组装。

题型参考(框架微调):

题目在框架上的微调点
[226] 翻转二叉树前序直接交换左右子树,或后序拿到左右结果后再组装。
[101] 对称二叉树把“比较同位”改成“比较镜像位”(左左对右右变左对右)。
[114] 二叉树展开为链表前序收集访问顺序后重连,或后序返回链表尾节点做拼接。

二、中序遍历:BST 有序操作

判断关键词:BST、有序性、第 K 小、前驱后继、区间统计。

适用场景:题目涉及 BST,且需要利用其有序性。

核心原理:BST 的中序遍历结果是有序的。

这一部分是「二叉树遍历」专题里对 BST 的重点补充:聚焦中序有序性在解题中的直接应用,而不展开为独立的数据结构增删改查专题。

2.1 BST 的性质与中序有序性

二叉搜索树(BST)只需抓住两点:

  1. 对于任意节点 node,左子树所有值都小于 node.val,右子树所有值都大于 node.val
  2. 任意节点的左右子树本身也都是 BST。

这两个特性直接推导出一个结论:BST 的中序遍历结果一定有序。所以涉及顺序统计(第 K 小、第 K 大、区间计数)时,中序遍历通常是首选。

例如在 [230] 二叉搜索树中第K小的元素 中,问题可以直接转化为“返回中序遍历的第 k 个节点值”:

ts
function kthSmallest(root: TreeNode | null, k: number): number {
  let rank = 0;
  let ans = 0;

  function dfs(node: TreeNode | null): void {
    if (!node || rank >= k) return; // 关键:可提前退出
    dfs(node.left);
    if (++rank === k) ans = node.val;
    dfs(node.right);
  }

  dfs(root);
  return ans;
}

题型参考(框架微调):

题目在 BST 框架上的微调点
[230] 二叉搜索树中第K小的元素中序遍历 + 计数器,命中第 k 个后提前退出。
[98] 验证二叉搜索树中序时维护 prev 保证严格递增,或用上下界递归。
[700] 二叉搜索树中的搜索不必完整遍历,按值大小只走一侧子树。
[235] 二叉搜索树的最近公共祖先利用大小关系剪枝:同在左/右则下探,否则当前即答案。

三、后序遍历:自底向上汇总结果

判断关键词:子树结果先算、返回值汇总、平衡/直径/路径和。

适用场景:当前节点的答案需要由左右子树的结果推导。这是二叉树中最常见的模式。

思维模式:分解问题思维为主——递归函数有明确返回值,在后序位置利用左右子树的返回值计算当前节点的结果。

3.1 属性计算类

求二叉树的某个属性值(深度、节点数、直径等)。共同特征:当前节点的答案可以由左右子树的答案直接推导。

例如在 [104] 二叉树的最大深度 中,当前节点的深度 = max(左子树深度, 右子树深度) + 1;在 [222] 完全二叉树的节点个数 中,节点数 = 左子树节点数 + 右子树节点数 + 1。

解题公式:当前节点属性 = f(left, right, node)

ts
function solve(node: TreeNode | null): number {
  // 返回“当前子树的核心属性”(如高度)
  // base case
  if (!node) return 0;

  const left = solve(node.left);
  const right = solve(node.right);

  // 可选:用 left/right 更新全局答案(例如直径、平衡性、路径最值)
  // updateGlobal(left, right, node.val);

  // 返回给父节点的值
  return Math.max(left, right) + 1;
}

示例映射:

  1. 最大深度:返回高度。
  2. 节点总数:返回 left + right + 1
  3. 二叉树直径:返回高度,同时用 left + right 更新全局最大直径。

3.2 任意路径最值

路径可以从任意节点出发到任意节点时,每个节点返回"经过该节点的单边最大路径和",在后序位置计算"双边路径和"并更新全局最优。

例如在 [124] 二叉树中的最大路径和 中,每个节点需要知道左右子树各自能贡献的最大路径和,这只有在后序位置才能拿到。

ts
function maxPathSum(root: TreeNode | null): number {
  let best = -Infinity;

  function gain(node: TreeNode | null): number {
    if (!node) return 0;
    const left = Math.max(0, gain(node.left));
    const right = Math.max(0, gain(node.right));
    best = Math.max(best, left + right + node.val); // 双边更新全局
    return Math.max(left, right) + node.val; // 单边返回父节点
  }

  gain(root);
  return best;
}

题型参考(框架微调):

题目在后序框架上的微调点
[104] 二叉树的最大深度返回值定义为“子树高度”,公式是 max(left, right) + 1
[110] 平衡二叉树返回高度时附带平衡性判断;常用 -1 作为失衡哨兵提前剪枝。
[222] 完全二叉树的节点个数通用后序可解,进阶可利用完全树性质按高度优化。
[124] 二叉树中的最大路径和返回单边贡献,双边路径只用于更新全局最优。
[543] 二叉树的直径124 同型:返回单边高度,统计双边路径长度。

四、BFS 层序遍历

判断关键词:逐层处理、最短步数、最小深度、队列扩散。

适用场景:涉及逐层处理、层级关系、最短路径。

标准代码模板见「前置知识 -> BFS 遍历框架」。本节不再重复模板,只关注如何按题意改造。

常见改造点

题型在骨架中的改动点
标准层序遍历(102)每层结束 res.push(level)
锯齿形层序遍历(103)按层号决定 level 是否反转后再入 res
右视图(199)每层取最后一个访问节点值
每层最大值(515)每层循环中维护 levelMax
最小深度(111)增加 depth;遇到首个叶节点立即返回
填充右侧指针(117)层内记录 prev,将 prev.next = node 逐个连接

BFS 的优势在于“按距离扩散”,所以最短路径/最小步数类问题通常优先考虑 BFS。

题型参考(框架微调):

题目在 BFS 骨架上的微调点
[102] 二叉树的层序遍历每层收集 level,层结束后 res.push(level)
[103] 二叉树的锯齿形层序遍历按层号决定当前层正序还是反序。
[107] 二叉树的层序遍历 II正常层序后反转结果,或改为头插结果。
[199] 二叉树的右视图每层取最后一个弹出的节点值。
[515] 在每个树行中找最大值层内维护 levelMax,层结束写入结果。
[111] 二叉树的最小深度增加 depth 计数,首次遇到叶节点立即返回。
[117] 填充每个节点的下一个右侧节点指针 II层内维护 prev 指针,顺序连接 prev.next = cur

五、构造类

判断关键词:由序列建树、确定根节点、划分左右区间。

适用场景:从遍历序列或其他条件构造一棵二叉树。

这一章是分解问题思维在“反向建树”场景的应用,不再重复递归遍历基础框架。

思维模式:分解问题思维——构造整棵树 = 确定根节点 + 递归构造左子树 + 递归构造右子树。

例如在 [654] 最大二叉树 中,根节点就是数组中的最大值,最大值左侧构成左子树,右侧构成右子树。所有构造类题目都遵循这一模式,差异仅在于"如何确定根节点"和"如何划分左右子树的范围"。

代码模板

ts
function build(
  order1: number[], start1: number, end1: number,
  order2: number[], start2: number, end2: number
): TreeNode | null {
  if (start1 > end1) return null;

  // 1. 确定根节点值(从前序/后序的首/尾元素获取)
  const rootVal = getRootVal(order1, start1, end1);
  const root = new TreeNode(rootVal);

  // 2. 在另一个序列中找到根节点位置,计算左子树大小
  const index = findIndex(order2, rootVal);
  const leftSize = index - start2;

  // 3. 递归构造左右子树
  root.left = build(order1, ..., order2, ...);  // 左子树范围
  root.right = build(order1, ..., order2, ...); // 右子树范围

  return root;
}

从遍历序列反构造二叉树

理解每种遍历结果的特点,是反构造的关键:

前序遍历:根节点一定是其结果的第一个。 中序遍历:根节点左边是左子树的中序结果,右边是右子树的中序结果。 后序遍历:根节点一定是其结果的最后一个。

通过前序或后序确定根节点值,在中序中找到根节点位置即可划分左右子树,进而得到各序列中左右子树的起止位置。

三种经典构造的核心差异

输入组合根节点位置结果唯一性
前序 + 中序前序首元素唯一
后序 + 中序后序末元素唯一
前序 + 后序前序首元素,前序第二个元素定位左子树不唯一

前序+后序结果不唯一,因为当节点仅有一棵子树时,无法判断其是左子树还是右子树。

优化技巧:使用 HashMap 存储中序遍历的 value → index 映射,避免每次递归都线性查找。

题型参考(框架微调):

题目在构造框架上的微调点
[105] 从前序与中序遍历序列构造二叉树根取前序首元素,在中序中定位根并按左子树大小切分区间。
[106] 从中序与后序遍历序列构造二叉树根取后序尾元素,其余划分逻辑与 105 对称。
[889] 根据前序和后序遍历构造二叉树用前序第二个元素定位左子树范围,但结果通常不唯一。
[654] 最大二叉树对照题:不依赖遍历序列,根由“当前区间最大值”直接确定。

六、序列化与子树比较

判断关键词:子树判等、重复子树、序列化签名、哈希统计。

适用场景:判断两棵树是否相同、是否为子树、寻找重复子树。

本章只讲“序列化签名”技巧;后序遍历的基础模板与返回值思路见第三章。

思维模式:分解问题思维——在后序位置将每棵子树序列化为字符串,用 HashMap 记录出现次数。

要拿到一棵子树的完整表示,必须先拿到左右子树结果再拼上当前节点,所以天然是后序位置。

ts
// 伪代码:后序序列化 + 哈希计数
serialize(node):
  if node == null: return "#"
  left = serialize(node.left)
  right = serialize(node.right)
  sig = left + "," + right + "," + node.val
  if count(sig) == 1: collect(node)
  count(sig) += 1
  return sig

题型参考(框架微调):

题目在序列化/比较框架上的微调点
[572] 另一棵树的子树主树每个节点都可作为候选根,做一次子树相同判断或签名匹配。
[297] 二叉树的序列化与反序列化关注编码与解码互逆;先定遍历顺序,再处理空节点占位。

七、最近公共祖先(LCA)

判断关键词:两个目标节点、公共祖先、左右子树各命中一个。

适用场景:寻找两个节点的最近公共祖先。

思维模式:分解问题思维——在后序位置,一个节点能知道左右子树中是否包含目标节点。

例如在 [236] 二叉树的最近公共祖先 中,我们需要知道 p 和 q 是否分别存在于左右子树中——这个信息只有在后序位置才能获得。如果左右子树各自找到了一个目标节点,当前节点就是 LCA。

经典 LCA 模板

ts
function lowestCommonAncestor(root: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null {
  if (!root || root === p || root === q) return root;

  const left = lowestCommonAncestor(root.left, p, q);
  const right = lowestCommonAncestor(root.right, p, q);

  // 后序位置:根据左右子树的结果判断
  if (left && right) return root;
  return left ?? right;
}

LCA 变体

变体关键差异处理方式
标准 LCA(236)p、q 一定存在标准模板
BST 的 LCA(235)利用 BST 有序性比较值大小,走左/右子树
节点可能不存在(1644)需确认两个节点都找到额外 boolean 标记
有父指针(1650)转化为链表交叉问题等同于相交链表(160)

BST 的 LCA 可以充分利用有序性优化:

ts
function lowestCommonAncestor(root: TreeNode | null, p: TreeNode, q: TreeNode): TreeNode | null {
  // 关键:利用 BST 有序性只走一边
  if (!root) return null;
  if (p.val < root.val && q.val < root.val) return lowestCommonAncestor(root.left, p, q);
  if (p.val > root.val && q.val > root.val) return lowestCommonAncestor(root.right, p, q);
  return root;
}

总结:解题速查表

题型遍历方式思维模式核心操作位置
根到叶路径前序遍历前序位置做选择,叶节点判断
结构判断与修改(翻转/对称)前序或后序两种皆可前序直接改 / 后序组装
BST 有序操作中序遍历中序位置处理有序性
属性计算(深度/节点数/直径)后序分解问题后序位置组合左右结果
任意路径最值后序分解问题后序位置更新全局最优
逐层处理/最短路径BFS遍历每层循环处理
构造二叉树(反向建树)递归分解分解问题按区间确定根并划分左右子树
子树签名比较/重复检测后序分解问题后序位置生成序列化签名
最近公共祖先后序分解问题后序位置判断左右结果

MIT LICENSE