Pattern 07 · Tree Traversal & Recursion

07 树的遍历与递归 一切树题 =「对当前节点做什么 + 递归左右」的填空

面试频率 极高题目 12 道模板 2 个⭐认脸 2 道
§1 Mental Model 一句话心智模型

先记住这一句

一切树题都是同一个填空:对当前节点做什么,加上递归左右孩子。动手前只需回答一句——这个递归函数的返回值代表什么?

把「返回值的含义」写在纸上(深度?子树是否合法?子树的最大路径和?),剩下的三件事会自动落位:空节点返回什么(地基)、怎么合并左右的返回值(当前节点做什么)、答案是不是就是根节点的返回值。

最容易卡住的一类是返回值 ≠ 答案:543 的返回值是深度、答案是直径;124 的返回值是「向上贡献的单边路径和」、答案是「经过某点的整条路径和」。这类题一律用 nonlocal best 把答案挂在递归外面。

§2 Recognition Signals 识别信号

看到什么,想到本章

题面出现…想到
「深度 / 高度 / 直径 / 是否平衡」后序递归,返回值是深度(模板 A)
「层 / 每一层 / 最短路径 / 右视图」BFS 队列,按层出队(模板 B)
「BST」+「第 K 小 / 有序输出」中序遍历天然升序
「验证 BST」带上下界递归,不能只和父节点比
「最近公共祖先 LCA」后序:左右都非空则当前就是答案;BST 可用值域直接下降
「前序 + 中序构造」「序列化 / 反序列化」用中序定位划分点 / 前序 + 空标记
分流条件 · 决策树同源

要枚举所有路径 / 所有方案并且需要撤销选择,那是回溯(第 09 章),树只是它的递归载体。网格和邻接表上的 BFS/DFS 属于第 10 章——区别在于树没有环、不需要 visited,一旦要判重就说明你面对的是图。「层序」和「多源最短步数」共用队列骨架,但树的层序不需要 visited。还有一个同名陷阱:树里说「有序」指的是 BST 的中序,和有序数组(第 05 章的二分)、有序链表(第 06 章的合并、第 08 章的 K 路归并)不是一回事——BST 的有序性藏在结构里,你得走一遍中序才能看见它。

§3 Template 模板代码

可默写的那几行

■ 【骨架】 不变,闭着眼默写 ■ 【插槽】 随题变化,写题前先问自己填什么

模板 A · 递归三问覆盖本章绝大多数题
def max_depth(root):
    if not root:                            # 【骨架】空节点的答案 —— 递归的地基
        return 0                            # 【插槽】空树返回什么
    left = max_depth(root.left)             # 【骨架】
    right = max_depth(root.right)           # 【骨架】
    return 1 + max(left, right)             # 【插槽】当前节点怎么合并左右的返回值
默写口诀:空的返回什么、左右各要什么、当前怎么合。返回值和答案不是一回事时,答案用 nonlocal 挂在外面。
模板 B · 层序 BFS凡是带「层」字的题
from collections import deque

def level_order(root):
    if not root:
        return []
    q, out = deque([root]), []              # 【骨架】
    while q:                                # 【骨架】
        size = len(q)                       # 【骨架】先固定这一层的人数,再开始出队
        level = []                          # 【插槽】这一层要收集什么
        for _ in range(size):               # 【骨架】
            node = q.popleft()              # 【骨架】
            level.append(node.val)          # 【插槽】
            if node.left:
                q.append(node.left)         # 【骨架】
            if node.right:
                q.append(node.right)        # 【骨架】
        out.append(level)                   # 【插槽】每层一组 / 只要最后一层 / 只要层数
    return out
size = len(q) 这一行是全部:先固定住这一层有几个人,再开始出队,否则新入队的下一层会混进来。
§4 Problems 题目分层

从建直觉到扭模板

热身建直觉,easy 为主2 道
  • 模板 A 的最小形态:当前节点交换左右,再递归。「对当前节点做什么 + 递归左右」这句话的字面演示。
  • 模板 A 原题。它的返回值恰好就是答案,是理解「返回值 ≠ 答案」之前最好的对照组。
标准模板直接套7 道
  • 第一道返回值 ≠ 答案的题:返回深度,答案是 l + r 的最大值。见 §1 第二段与翻车点①。
  • 朴素写法对每个节点都算一次深度是 O(n²)。正解让 depth 返回 -1 表示「已经不平衡」,一次遍历搞定——剪枝藏在返回值里。
  • 模板 B 原题。107(自底向上)把结果 reverse 即可,199(右视图)取每层最后一个,都是同一个骨架。
  • 必须带 (lo, hi) 上下界往下传。只比父节点的写法会放过 [5,4,6,null,null,3,7] 这种用例——模板测试里专门留了这条断言。
  • BST 中序遍历是升序,走到第 K 个就返回。面试常追问「频繁查询怎么办」:答案是节点里维护子树大小。
  • BST 版 LCA:两个值都比当前小就往左,都比当前大就往右,否则当前节点就是分叉点。不需要递归回溯,一路向下即可。
  • 普通二叉树版 LCA:后序递归,左右子树的返回值都非空 → 当前节点就是 LCA。和 235 放在一起看,能清楚感受到「BST 这个条件值多少钱」。
变体模板要扭,坑的来源3 道
§5 Pitfalls & Self-check 常见翻车点 + 自测

会写不等于不翻车

  1. 返回值和答案混为一谈——543 / 124 / 110 都是这一类。写之前先说一句「这个函数返回的是 X」,如果答案不是 X,就必须另开一个 nonlocal 变量。
  2. 验证 BST 只比父节点——node.left.val < node.val 只保证局部有序。必须把祖先施加的上下界一路传下去。
  3. 层序忘了先取 len(q)——循环里直接 while q 逐个出队,会把下一层的节点混进当前层。凡是要「按层」的输出,第一行永远是 size = len(q)。
  4. 平衡判断退化成 O(n²)——在 is_balanced 里对每个节点调用 depth,最坏是 O(n²)。用「返回 -1 表示已失衡」把两件事合并成一次遍历。
  5. 递归深度——退化成链的树(1e5 个节点)会爆 Python 默认递归上限。面试里提一句「必要时改迭代或调 setrecursionlimit」是加分项。

自测 · 30 秒识别模式

30 秒识别Q1 · 572 Subtree of Another Tree:判断一棵树是不是另一棵树的子树。你的第一反应是哪个模式?

看答案

树的递归。外层递归找候选根,内层递归判「两棵树是否完全相同」——两个模板 A 嵌套。看到「结构相同」就该想到 isSame(a, b) 这个经典返回值。

30 秒识别Q2 · 199 Binary Tree Right Side View:从右侧看一棵二叉树,返回从上到下能看到的节点值。你的第一反应是哪个模式?

看答案

层序 BFS(模板 B)。每层只取最后一个,size = len(q) 那行的直接应用。「右视图 / 每层最大值 / 层平均数」全是同一道题换皮。

30 秒识别Q3 · 200 Number of Islands:给一个由 '1'(陆地)和 '0'(水)组成的网格,数出岛屿的个数。你的第一反应是哪个模式?

看答案

图的 BFS/DFS,不是本章——网格是图不是树,会绕回来,所以必须有 visited(或就地改成 '0')。树的递归之所以不用判重,是因为树天然无环。见第 10 章。

每章自测都会混入恰好一道非本章题,防止「读哪章选哪章」的条件反射;题面也一律取自本章未收录的题,避免凭记忆而非凭信号作答。