🏠 总目录📚 本教程 02 · 树:递归到底在替你保存什么 ← →
📑 本页目录(点开跳转)

02 · 树:递归到底在替你保存什么

⏱ 10 分钟 | 深度、层序、路径和、最近公共祖先


🎯 一句话

树题先别问“怎么递归”;先问:一个节点要把什么答案交回给它的父节点?

🧩 题 1:二叉树的深度

叶子节点的深度是 1;非叶子节点只需拿左右孩子的答案,取较大再加 1。

class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right


def depth(root):
    if root is None:
        return 0
    return 1 + max(depth(root.left), depth(root.right))


root = TreeNode(1, TreeNode(2, TreeNode(4)), TreeNode(3))
assert depth(root) == 3

递归契约:depth(node) 永远返回“以 node 为根的子树有多高”。父节点完全不需要知道孩子内部怎么数。

🧩 题 2:从上往下打印二叉树

这不是递归题,而是“先来的先处理”:用队列做层序遍历。

from collections import deque


class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right


def level_order(root):
    if root is None:
        return []
    queue, answer = deque([root]), []
    while queue:
        node = queue.popleft()
        answer.append(node.val)
        if node.left:
            queue.append(node.left)
        if node.right:
            queue.append(node.right)
    return answer


root = TreeNode(1, TreeNode(2, TreeNode(4)), TreeNode(3))
assert level_order(root) == [1, 2, 3, 4]

🧩 题 3:二叉树中和为某一值的路径

关键不是找路径,是回去时清干净。 path 记录当前这条根到节点的路;走进一个节点时加入,离开它时弹出。

class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right


def path_sum(root, target):
    answer, path = [], []

    def visit(node, remain):
        if node is None:
            return
        path.append(node.val)
        remain -= node.val
        if node.left is None and node.right is None and remain == 0:
            answer.append(path[:])  # 复制当前答案,不能直接塞 path
        visit(node.left, remain)
        visit(node.right, remain)
        path.pop()                 # 回到父节点前复原现场

    visit(root, target)
    return answer


root = TreeNode(5, TreeNode(4, TreeNode(11, TreeNode(7), TreeNode(2))), TreeNode(8))
assert path_sum(root, 22) == [[5, 4, 11, 2]]

⚠️ answer.append(path) 是高频错:所有答案都指向同一个会继续变化的列表,最后会一起变空。

🧩 题 4:两个节点的最近公共祖先

若当前节点就是 a 或 b,把自己交回去;左右子树各交回一个目标,当前节点就是交汇点;只交回一边,就把那一边继续交给父节点。

class TreeNode:
    def __init__(self, val, left=None, right=None):
        self.val, self.left, self.right = val, left, right


def lowest_common_ancestor(root, a, b):
    if root is None or root is a or root is b:
        return root
    left = lowest_common_ancestor(root.left, a, b)
    right = lowest_common_ancestor(root.right, a, b)
    if left and right:
        return root
    return left or right


n4, n5, n2 = TreeNode(4), TreeNode(5), TreeNode(2)
root = TreeNode(1, n2, TreeNode(3))
n2.left, n2.right = n4, n5
assert lowest_common_ancestor(root, n4, n5) is n2

这题的递归契约更具体:lowest_common_ancestor(node, a, b) 会交回“在这棵子树里找到的目标节点,或已经确定的公共祖先”。


🔗 这一章连到哪里

去哪 为什么
不靠数据的 AI 06 · 无信息搜索 ⭐ 递归替你保存的那个栈,那一章把它显式写了出来:DFS 用栈、BFS 用队列,两个算法只差这一行。本章题 2 的层序打印就是那里的 BFS

🛑 可以停在这里

树的递归不是魔法:每层只做“处理当前节点 + 向孩子要答案 + 把自己的答案交回去”。


✅ 检查点

路径和题为什么离开节点时必须 path.pop()?

👀 看答案

因为同一个 path 会被左右子树共用。离开左子树不弹出,右子树会带着一段不属于它的旧路径继续走。


⚡ 走神救援

继续: 03 · 栈、队列和窗口:谁应该先出来

打卡记录保存在你的浏览器里,首页能看到总进度