Theory · Data Structure · Binary Tree

二叉树与遍历

递归的艺术:四序遍历 · 迭代与 Morris · BST · 构造与序列化 —— LeetCode 树题家族的总纲

遍历是地基

前中后层四序 + 递归/迭代/Morris 三写法,覆盖 80% 树题的骨架

递归三问

整棵树 = 根 + 左右子树;想清楚"函数返回什么"就解了 80%

BST 是秩序

左小右大 → 中序即有序;插入/删除/验证全靠这条不变式

定位:二叉树是递归思维的训练场,也是面试树形题(含堆、红黑树、 Trie)的公共前置。三条主线:遍历写法、递归框架、BST 有序性。

Why It Matters

先看两个绕不过去的问题:数据本身会分叉,查找本身也在分叉

问题一:有些数据天生不是一条线

文件夹里套文件夹、评论下面挂回复、网页的标签嵌标签、算式 (1+2)×3 的运算顺序——这些都是"一个东西下面挂着几个东西"。用数组或链表只能勉强编码,不能自然表达

问题二:查找快和插入快,能不能都要

有序数组能二分(100 万条只要约 20 次比较),但插一条要挪一半元素;链表插一条很便宜,但查找只能从头走。两个都想要,就必须换结构。

关键一步:把二分的过程"画下来"

在 1..7 里二分查 6:先看中间的 4(小了,往右),再看 6 的那一段的中间 6(找到了)。把每次"看的那个数"当作一个节点、"往左/往右"当作两条分叉画出来——你画出的正是一棵树,而且它满足"左边都比我小、右边都比我大"。这就是二叉搜索树 BST

于是树同时解决了两个问题

它能自然表达层级关系,又把"二分的每一步"固化成了节点:查找沿着分叉走 O(树高),插入只改几个指针,不用挪动任何其他数据。代价是要小心树的形状——一旦长歪成一条链,O(log n) 就退回 O(n)。

需求有序数组链表二叉搜索树
按值查找 O(log n)慢 O(n) O(树高)
插入 / 删除慢 O(n) 挪数据 O(1) O(树高)
表达层级嵌套不行不行天然
按顺序输出全部天然有序要排序中序遍历即有序
一个必须记住的前提:表里写的是 O(树高) 不是 O(log n)。100 万个节点,形状好时树高约 20,最坏(按升序依次插入)会长成一条 100 万长的链——操作从 20 步变成 100 万步。这就是平衡树那一篇存在的全部理由。
本 deck 路线:① 认清形态与高度(形状决定一切);② 学会四种遍历——它们是所有树代码的骨架;③ 掌握"递归三问"这个万能起手式;④ 落到 BST 的查/插/删/验证;⑤ 最后看完全二叉树怎么塞进数组(这是的地基)。LeetCode 两百多道树题,共用的就是这套骨架。
动机页两条腿:① 现实数据本身分叉(线性结构表达不了);② 把二分过程画成图就得到 BST——由具体到抽象的桥。右侧表刻意写 O(树高) 而非 O(log n),为"退化"和平衡树埋线。全页不出现任何结构定义。

Prerequisites & Glossary

先把词认全:下面每一页都会用到它们

术语一句话理解(下一页配图逐个标出)
节点 node / 边 edge装一个值的小方块;边就是"谁是谁的孩子"的连线
根 root / 叶 leaf最上面没有父亲的那个是根;下面没有孩子的是叶
子树 subtree任取一个节点连同它所有后代——本身又是一棵完整的树
深度 depth某节点到根有几条边(根的深度 0)——从上往下数
高度 height最深的叶子的深度——从下往上数;高度决定操作要走几步
度 degree一个节点有几个孩子;二叉树里只能是 0、1、2
完全二叉树除最后一层外每层填满,最后一层靠左连续——能直接塞进数组
遍历 traversal把每个节点不重不漏访问一次的过程
前 / 中 / 后序指的都是"什么时候处理根":先处理根 / 夹在左右之间 / 最后处理
BST 二叉搜索树每个节点都满足"左子树都比我小、右子树都比我大"的树

前置知识:不确定就先看这两篇

数组与链表 → 节点+指针的写法、以及"为什么挪数据很贵"
复杂度分析 → O(log n) 的直觉:每一步砍掉一半

最小心智模型(记住这一句)

树的定义本身是递归的:一棵树 = 一个根 + 左子树 + 右子树(都可以为空)
所以处理树的所有代码都是同一个动作:假设左右子树的答案已经算好了,我只负责用它们拼出自己的答案——空树是递归的终点。

四种遍历只是这个动作的四个时机

在"处理左、处理右"这两步之间,把"处理根"插在哪里,就得到前序(根最先)、中序(在中间)、后序(根最后);换成按层横着走就是层序。把"根什么时候被处理"想清楚,遍历就不用背了。

阅读提示:后面的"递归三问"页就是这个心智模型的操作化版本;实在想不起某个词,回来查这一页。
前置页:十个术语先定义再使用,重点澄清"深度从上往下 / 高度从下往上"和"前中后序说的都是根的处理时机"这两个高频混淆点。最小心智模型(树=根+左右子树,假设子树答案已有)是全 deck 的统一动作。

Terminology · Shapes

术语与四种关键形态

上一页的词在这张图里逐个标出;右侧三种形态请重点看最后一种——退化成链就是开场说的"O(log n) 变 O(n)"。

二叉树的术语标注与满、完全、退化三种形态对比 左边一棵七节点二叉树标注根、叶、深度和高度;右边对比满二叉树、完全二叉树和退化成链表的树,说明形态直接决定高度上限与操作效率。 ABC DEFG root 根(深度 0) 叶子节点:D、E、F、G(无孩子) 深度 depth:到根的边数 高度 height:最深叶子的深度 子树:任何节点连同其后代 高度决定一切:n 节点正常形态高 O(log n),退化形态高 O(n) 递归遍历栈深 = 树高;完全二叉树可数组化(→ 堆) FULL · 满二叉树(每层全满) COMPLETE · 完全(层序靠左) DEGENERATE · 退化成链
术语只强调两组:深度(到根)与高度(到最深叶);形态三兄弟——满、完全(堆的前提)、退化(BST 最坏形态)。"高度决定一切"这句是后面所有复杂度结论的根。

Properties · With Proofs

三条必会性质,附一行推导

① 第 i 层最多 2^(i−1) 个节点

归纳:第 1 层 1 = 2⁰;每层节点最多翻倍。n 个节点、度数为 2 的树高度下界 ⌈log₂(n+1)⌉——这是"树操作 O(log n)"的来源。

② 叶子数 = 度 2 节点数 + 1(n₀ = n₂ + 1)

边数守恒:树边数 = 节点数 − 1;每条边从父出发 → n₁ + 2n₂ = n₀ + n₁ + n₂ − 1 → n₀ = n₂ + 1。面试常让你现场推。

③ n 节点完全二叉树高度 = ⌊log₂n⌋

完全树前 h 层满有 2^h − 1 ≥ n → h ≤ log₂(n+1);且 2^h ≤ n(第 h+1 层至少 1 个)→ h = ⌊log₂n⌋。堆的操作复杂度全靠它。

事实数值 / 公式
n 节点二叉树边数n − 1
满二叉树 k 层节点总数2^k − 1
n 节点完全树叶子数⌈n/2⌉(数组存储时下标 ≥ n/2)
Catalan 数:n 节点二叉树形态数C(2n,n)/(n+1)——不同形态指数多
为什么"高度 = O(log n)"如此重要?所有"自根向下走"的操作(查找、插入、堆化)都是沿一条根叶路径——代价 = 高度。平衡树的一切努力(平衡树 deck)就是强行维持这个对数下界。
三条性质各配一行推导,要求面试现场能写:层数翻倍归纳、边数守恒、完全树夹逼。表格最后一行 Catalan 数是趣味加分项——说明"树"的状态空间是指数级的,遍历是遍不完的。

Four Traversals · Recursive

四序遍历:递归版是一切写法的母版

// DFS 三序:只差"处理自己"的位置
func dfs(root *TreeNode) {
    if root == nil { return }
    // 前序位置:进入节点时(根左右)
    dfs(root.Left)
    // 中序位置:左子树处理完(左根右)
    dfs(root.Right)
    // 后序位置:两棵子树都完(左右根)
}
// BFS 层序:队列逐层推进
func levelOrder(root *TreeNode) [][]int {
    if root == nil { return nil }
    q := []*TreeNode{root}; res := [][]int{}
    for len(q) > 0 {
        n := len(q); layer := []int{}
        for i := 0; i < n; i++ {
            cur := q[0]; q = q[1:]
            layer = append(layer, cur.Val)
            if cur.Left != nil { q = append(q, cur.Left) }
            if cur.Right != nil { q = append(q, cur.Right) }
        }
        res = append(res, layer)
    }
    return res
}

前序:自顶向下的"路径"视角

适合"进入节点时做事":序列化、复制树、路径收集、前缀树构建。

中序:BST 专属的"有序"视角

BST 中序 = 升序序列——验证 BST、第 k 小(LC 230)、找中位数全靠它。

后序:自底向上的"汇总"视角

适合"需要孩子答案":高度、直径、LCA、删除释放——后序是分治的位置

层序:逐层的"广度"视角

最小深度、右侧视图(199)、每层最大值、之字打印——凡"按层"皆 BFS。

复杂度:四序都是时间 O(n)(每节点访问一次);空间 = 栈/队列 O(h) 或 O(w)——最坏退化树 O(n)。
核心记忆:三序递归代码完全相同,只有"处理自己"那一行的位置不同——前序做事、中序出序、后序汇总。右栏"四种视角对应四类题"是选题信号词。

Same Tree · Three Orders

同一棵树,三种序列对照

同一棵二叉树的前序、中序、后序序列对照 根 A 左 B 右 C,B 下有 D 和 E 的六节点树;前序根左右得 A B D E C F,中序左根右得 D B E A F C,后序左右根得 D E B F C A。 ABC DEF 左子树 B(D,E) · 右子树 C(F) 前序 PRE · 根左右 A · B · D · E · C · F 中序 IN · 左根右(BST 里 = 升序) D · B · E · A · F · C 后序 POST · 左右根(分治汇总位) D · E · B · F · C · A 首/末元素都是根:前序首 = 后序末 = A —— 构造树时靠它定根
建议现场用这棵六节点树手推三个序列,30 秒内出答案。底部注释是构造题的伏笔:前序定根(首元素)+ 中序分左右,就是 LC 105 的全部原理。

Iterative · Morris O(1) Space

迭代遍历:显式栈与 Morris 线索化

显式栈:把调用栈搬到堆上

递归的本质是系统栈,迭代版=自己管理:前序最好写(根入栈→弹→右先左后压);中序"一路向左压栈、弹出访问、转右";后序用"前序变形(根右左)再反转"最简洁。

统一写法:颜色标记法

栈里放 (节点, 是否已访问):白色未访问——弹出时按倒序压孩子并标灰自己;灰色直接输出。一套代码出三序,免背三个模板。

Morris 遍历:O(1) 空间的狠活

利用"叶子的空指针":找当前节点的中序前驱(左子树最右),在其右指针上挂回当前节点的线索。有线索→走左边;已有线索→访问自己、拆除线索、转右。时间 O(n)(每边最多走两次),空间 O(1)。

工程取舍

日常写递归(清晰);深树防爆栈用显式栈;内存敏感/面试深挖才上 Morris——代价是临时破坏树结构,并发下不安全。

// 中序迭代:一路向左 + 栈回溯
func inorder(root *TreeNode) []int {
    stack, res := []*TreeNode{}, []int{}
    cur := root
    for cur != nil || len(stack) > 0 {
        for cur != nil {   // 左链全压栈
            stack = append(stack, cur)
            cur = cur.Left
        }
        cur = stack[len(stack)-1]  // 弹出=访问
        stack = stack[:len(stack)-1]
        res = append(res, cur.Val)
        cur = cur.Right            // 转右
    }
    return res
}
为什么面试爱问迭代版?考察你是否真懂"递归=栈":能把递归写法机械翻译成显式栈,说明理解了执行模型而不只是背模板。Go 无尾调用优化,深树迭代是硬需求。
三层递进:显式栈(机械翻译递归)→ 颜色标记法(统一三序)→ Morris(O(1) 空间,用前驱线索)。Morris 的两个关键词:中序前驱、线索拆除后恢复。Go 无 TCO 让迭代版从"加分"变"刚需"。

Level Order · Queue

层序遍历族:凡"按层"皆 BFS

分层技巧:先记下当前层长度

进入每轮循环前 n = len(q),恰好弹 n 个——它们就是一整层。没有这个"快照",队里新旧元素混在一起就分不了层。

之字形(LC 103)

分层基础上加奇偶标志:偶数层正序、奇数层把本层结果反转(或用 deque 两端出入)。只动"收集方向",不动遍历顺序。

右侧视图(LC 199)

每层最后一个元素入选——分层后取 layer[n−1]。左侧视图/每层最大值(515)同款换判定位置。

最小深度(LC 111)的陷阱

最小深度 = 到最近叶子,不是到最近空孩子:某节点只有左孩子时,深度必须算到左子树的叶子。BFS 第一个遇到的叶子即答案(比 DFS 全扫快)。

题目变形要点
102 层序遍历 M分层快照 n = len(q)
103 锯齿形 M奇偶层收集方向反转
199 右侧视图 M每层末尾元素
515 每层最大值 M层内聚合 max
111 最小深度 E首个叶子即停(叶子=无孩子)
116 填充右侧指针 M层内串链:cur.Next 指向队头
复杂度:时间 O(n) 每节点一次;空间 O(w)——w 为最大层宽,最坏(满树底层)n/2。栈与队列 deck 里"队列=逐层公平"在这里兑现。
层序家族的钥匙只有一个:n=len(q) 分层快照。之后所有变形(之字/视图/每层聚合)都只是"收集策略"变化。111 的"最小深度≠最近空孩子"是送命题级细节。

Recursion Framework

递归三问:树题的万能起手式

第一问:整棵树和子树是什么关系?

把"对整棵树求 X"翻译成"根做一点事 + 左子树求 X + 右子树求 X"。想不出递归时,先画三层找规律。

第二问:函数返回什么?

这是最重要的决定:返回高度(104/110)、返回节点(236 LCA)、返回"是否"(101/98)、返回新根(226 翻转)。返回值定了,递归体就定了

第三问:跨界信息怎么传?

父→子用参数(路径、上下界),子→父用返回值,全局最优用外部变量(直径 543、最大路径和 124——返回值给父用、同时更新全局答案)。

和回溯/分治什么关系?

前序做事+收集=回溯(回溯 deck);后序汇总=分治;遍历本身就是 DFS(图算法 deck 把它推广到一般图)。

// 直径 (LC 543):三问示范
// Q2: 返回高度给父用
// Q3: 全局变量记最优(左深+右深)
var ans int
func diameterOfBinaryTree(root *TreeNode) int {
    ans = 0
    depth(root)
    return ans
}
func depth(n *TreeNode) int {
    if n == nil { return 0 }
    l, r := depth(n.Left), depth(n.Right)
    if l+r > ans {          // 过根的路径
        ans = l + r
    }
    if l > r { return l + 1 }
    return r + 1            // 只能选一边向上
}
关键细节:路径不能同时拐两条腿——返回给父的只能是一条边(l+1 与 r+1 取大),而"l+r"只在本地参与全局答案比较。这个"返回值 vs 全局值"的分离是 124 最大路径和的同款套路。
三问框架是本 deck 的方法论核心:关系分解→返回值设计→信息传递通道(参数/返回值/全局变量)。LC 543 代码把三个通道全用上了,值得逐行讲。

LeetCode Family Tree

树题族谱:按套路分组必刷

家族题目(编号 · 难度)核心套路
深度/平衡104 最大深度 E · 110 平衡二叉树 E · 111 最小深度 E后序汇总高度;平衡 = 左右高差 ≤1 全树成立
结构变换226 翻转 E · 617 合并两棵树 E · 114 展开为链表 M返回新根,子问题拼装
对称/相同100 相同的树 E · 101 对称二叉树 E双参递归:左右镜像同步走
路径类112 路径总和 E · 257 所有路径 E · 543 直径 E · 124 最大路径和 H参数传路径状态 / 全局变量记最优
祖先类236 最近公共祖先 M · 235 BST 版 M后序返回命中;BST 版走分支
BST 操作98 验证 M · 230 第 k 小 M · 701 插入 M · 450 删除 M · 108 有序数组建树 E中序有序 + 上下界
构造/序列化105 前中建树 M · 106 中后建树 M · 297 序列化 H定根 + 切分中序(下一页)
刷法建议:104/226/101 热身(每题 5 分钟)→ 236/105 中坚 → 124/297 收尾。每题先答"三问"再写码——面试官听的就是你的分解过程。
族谱七行覆盖了 LC 树题 90% 的套路。强调"先答三问再写码":面试评分点在分解过程,代码只是分解的产物。

Lowest Common Ancestor · LC 236

图解:最近公共祖先的后序递归

最近公共祖先问题的后序递归解法 在二叉树中寻找 p 和 q 的最近公共祖先:后序遍历自底向上返回命中标记,左右子树各命中一个的节点即为答案;p 或 q 自身也是合法祖先。 351 6208 74 p = 4 · q = 8 null 4 命中 → 2 返回 4 8 命中 → 1 返回 8 左右都非空 → 3 是 LCA 递归定义(返回值 = 命中节点) nil → 返回 nil 命中 p 或 q → 返回自己 左返回 L,右返回 R: L、R 都非空 → 我就是 LCA 只有一个非空 → 透传它 都空 → 返回 nil 时间 O(n) · 空间 O(h) p、q 互为祖先时也成立(自举)
LCA 是"返回值设计"的典范题:返回值=命中标记,聚合规则三条。蓝色节点是 p=4、q=8,命中标记沿树向上透传,在根 3 处左右都非空——3 即 LCA。BST 版可简化:走分支找分叉点。

Build & Serialize · LC 105/106/297

构造与序列化:两个序列怎么唯一确定一棵树

核心原理:定根 + 切分

前序首元素(或后序末元素)= 根;中序里根的左边全在左子树、右边全在右子树 → 递归切分。LC 105(前+中)、106(中+后)同构。

为什么必须带中序?

前+后序都知道根在哪,但分不开左右:单孩子时无法判断它在左边还是右边——前+后序不能唯一建树(每个内部节点度数都为 2 的"真二叉树"除外)。

实现细节

用哈希表存"中序值→下标"避免每次线性查找;区间用闭区间 [inL, inR] 与前序偏移量 preIdx 递增——preIdx 是跨递归的全局游标(root 先建,先左后右的顺序不能反)。

序列化(LC 297)

前序 + null 占位符可以唯一反序列化(null 补齐了结构信息);层序 + null 同理。反序列化用同一个"游标"逐个消费。LeetCode 官方输入格式就是层序+null。

序列组合能唯一建树?原因
前序 + 中序根定位 + 左右切分
后序 + 中序同上(根在末尾)
层序 + 中序层序逐层给根
前序 + 后序✗(真二叉树除外)单孩子无法定边
面试金句:"序列化问题的本质是结构信息量——中序提供'左右分界'信息,缺了它,任何两个 DFS 序都留有二义性。null 占位符则是在一个序列里手动补齐结构信息的做法。"
记忆锚:"定根+切分"四字。必须能说出为什么前+后不够(单孩子二义性)——这是这道题真正的考点。哈希索引中序、全局 preIdx 游标、先左后右,三个实现细节一个都不能少。

Binary Search Tree

BST:一条不变式撑起全部操作

不变式:左子树 < 根 < 右子树(对每个节点)

推论一:中序遍历 = 升序序列(验证/第 k 小/范围查询的地基);推论二:查找/插入都沿单一路径,O(h);推论三:退化成链时 h=n → 平衡树接管。

查找与插入

与当前节点比较,小往左大往右——像二分但走的是树。插入总是落在空位上(新节点永远是叶子)。

删除(LC 450)三情况

① 叶子:直接删;② 单孩子:子树上提顶替;③ 双孩子:用中序后继(右子树最左节点,或等价地中序前驱)的值覆盖根,再删掉那个后继——后继最多一个右孩子,回到情况①②。

中序后继/前驱怎么找

后继 = 右子树最左;无右子树时 = 第一个"从左侧拐下来"的祖先。这就是 Morris 遍历找线索用的同一个指针游戏。

操作平均最坏(退化)
查找O(log n)O(n)
插入O(log n)O(n)
删除O(log n)O(n)
中序遍历O(n)O(n)
max/minO(log n)O(n)
面试金句:"BST 的所有操作代价都是 O(h),h 夹在 log n 和 n 之间——AVL/红黑树的全部存在意义就是把 h 按住在对数。随机插入期望 O(log n),但有序输入必退化,所以生产环境从不用裸 BST。"
一条不变式推三个推论(中序有序、O(h)、退化风险)。删除三情况是操作题核心——第③种的"换值+降级"技巧要讲清楚:后继最多单孩子,所以换值后删除必然落入简单情况。

Validate BST · LC 98

验证 BST:经典陷阱题的三种写法

错误写法:只比较父子

left.Val < root.Val < right.Val 只保证局部有序——反例:5 → (1, 6 → (4, 7)),4 在 6 的左子树却大于根 5。BST 要求的是整棵子树的范围约束。

正确一:上下界传递

递归带 (min, max) 边界:进左子树把上界收紧为当前值,进右子树下界收紧为当前值;用 nil 表示无界(避免 int32/int64 边界值陷阱)。

正确二:中序严格递增

BST ⇔ 中序序列严格递增。维护 prev 前驱指针,中序遍历中逐一比较——不用数值边界,天然免疫边界值问题。

陷阱清单

① 节点值 = int 边界(LC 故意用 −2³¹):比较用 nil 界或前驱指针;② "严格递增"不是"非递减"——相等即非法;③ 只查直接孩子是高频错误写法,主动说出来就是加分。

// 上下界法:nil = 无界,免疫边界值
func isValidBST(root *TreeNode) bool {
    return check(root, nil, nil)
}
func check(n, lo, hi *TreeNode) bool {
    if n == nil { return true }
    if lo != nil && n.Val <= lo.Val {
        return false
    }
    if hi != nil && n.Val >= hi.Val {
        return false
    }
    return check(n.Left, lo, n) &&
           check(n.Right, n, hi)
}
一题双考点:表面考 BST 定义(全局范围约束),实际考边界思维(int 边界值)。两种正确写法都要能写——上下界版更好讲,中序版更优雅。
先演示错误写法再给反例(5→(1,6→(4,7))),是这题的教学关键——"局部有序≠全局有序"。代码用 nil 做无界哨兵,直接绕开 MinInt 陷阱,这个技巧可以平移到所有"数值边界"题。

Complete Tree As Array

完全二叉树的数组化:堆的地基

完全二叉树的数组存储与父子下标公式 完全二叉树按层序放进数组,下标 i 的父节点是 i 减一除二,左孩子是 2i 加一,右孩子是 2i 加二;这就是堆用数组实现而不用指针的原理。 ABC DEFG i=012 3456 ABCD EFG 0123 456 按层序(BFS 序)存入数组 —— 完全性保证"无空洞" parent(i) = (i−1)/2 · left(i) = 2i+1 · right(i) = 2i+2 BFS 序落盘
为什么只有"完全"二叉树能数组化:层序落盘不能有空洞,否则下标公式失效。三个下标公式是堆(下一站)的全部数学基础——上浮/下沉就是在这条父子链上交换。1-based 写法(2i / 2i+1)也要知道,别在面试时愣住。

Cheat Sheet

一页带走:遍历、递归套路、BST、复杂度

① 四种遍历:选哪一种

遍历根的处理时机什么时候用它
前序根 → 左 → 右自顶向下传信息:复制树、打印路径、序列化
中序左 → 根 → 右BST 专用:输出即升序、验证 BST、找第 k 小
后序左 → 右 → 根自底向上汇总:求高度/直径、删除释放、平衡判定
层序按层横着走(队列)逐层输出、之字形、右视图、最小深度

② 递归三问(写树题的固定起手)

第一问整棵树的答案怎么由左右子树的答案拼出来?
第二问递归函数返回什么(高度?是否合法?子树最大和?)
第三问跨子树的信息怎么传:参数往下传(上下界、路径),返回值往上带(汇总)
终止条件几乎总是 if root == nil;先写它再写别的

③ BST 必背四条

不变式每个节点:左子树全部 < 它 < 右子树全部(不是只比父子)
中序即有序中序遍历输出严格递增——验证 BST 与找第 k 小都靠它
删除三情况叶子直接删;一个孩子用孩子顶上;两个孩子用右子树最小(或左子树最大)替换后递归删那个
验证陷阱只比较父子是错的;用上下界传递,边界用开区间,注意 int 极值与重复值

④ 复杂度与数组化

遍历时间 O(n);空间 O(树高)(递归栈),最坏 O(n),Morris 可做到 O(1)
BST 查/插/删O(树高):平衡时 O(log n),退化成链时 O(n)
完全二叉树数组化下标从 0 起:左孩子 2i+1、右孩子 2i+2、父 (i−1)/2
两条性质第 i 层最多 2i−1 个;叶子数 n₀ = 度 2 节点数 n₂ + 1
一句话背下来:树 = 根 + 左右子树,所以先想"假设子树答案已有";遍历的差别只是根什么时候被处理;而所有复杂度都写作 O(树高)——形状不受控就该换平衡树。
速查页四块:遍历选型、递归三问、BST 四条、复杂度与数组化公式。刻意把"O(树高)"写在最后并回指平衡树 deck,与开场页首尾呼应。

Interview QA · Part 1

高频追问:遍历与递归

先盖住答案自己答一遍,再展开对照——想不起来比看得顺眼记得牢;答不出的翻回上一页速查表。

1 · 为什么树的遍历天然适合递归?

自相似结构

树 = 根 + 两棵更小的树,子问题与原问题同构——递归是这种自相似的直译。调用栈深度 = 树高,退化树 O(n) 有爆栈风险(Go 无 TCO)。

2 · 三序递归怎么改成迭代?

显式栈颜色标记

机械翻译:栈存节点与访问状态。前序最好写;后序可"根右左前序 + 反转"取巧;颜色标记法(白=待访问、灰=已访问)一套代码出三序,不用背三个模板。

3 · Morris 遍历为什么能 O(1) 空间?

前驱线索

用叶子的空右指针存"回当前节点"的线索(中序前驱指向后继):有线索→进左子树,访问完拆除线索恢复原树。时间 O(n)(每条边至多走两次),代价是遍历期间临时改结构、并发不安全。

4 · 层序"分层"的关键是什么?

len(q) 快照

每轮循环前记录 n=len(q),本轮恰好弹 n 个——正好是一层。之后所有变形(之字 103、右视图 199、每层最大 515)都只改收集策略。

5 · n₀ = n₂ + 1 怎么证明?

边数守恒

树边数 = 节点数 −1;从出边看 n₁+2n₂ = n₀+n₁+n₂−1 → n₀ = n₂+1。面试要求现场推,30 秒版:每加一个度 2 节点净多 1 个叶子。

6 · 完全二叉树为什么能用数组存?

无空洞下标公式

层序落盘不留空洞,parent(i)=(i−1)/2、left=2i+1、right=2i+2 全部成立——零指针开销、缓存友好。这就是堆的实现方式(堆 deck)。

7 · 最小深度为什么不能照抄最大深度?

LC 111 陷阱

最小深度到最近叶子。节点只有单侧孩子时,min(左深,右深) 会错把"空孩子当叶子"——必须特判单孩子(或用 BFS 首个叶子即停)。

8 · 递归树题怎么避免"想了半天写不出"?

三问框架

① 整树与子树关系;② 函数返回什么(高度/节点/bool/新根);③ 跨子树信息走参数(向下)、返回值(向上)、全局变量(旁路)。三问答完代码就剩翻译。

前八题覆盖遍历与递归方法论:迭代三写法、Morris 原理与代价、分层快照、性质证明、数组化、111 陷阱、三问框架。第 3 题要主动说出 Morris 的"并发不安全"代价,显示工程意识。

Interview QA · Part 2

高频追问:BST、构造与组合

同样先自答再对照。这一页的题要"结论 + 一句推导/边界"才完整——只答结论会被继续追问。

9 · 建树为什么必须中序?

定根+切分

前序/后序能定根,中序能分左右——两者缺一不可。前+后序在单孩子节点上二义(孩子在左还是右无法判断),不能唯一建树(真二叉树除外)。

10 · 验证 BST 常见错误?

局部 vs 全局

只比父子(5→(1,6→(4,7)) 反例);用 int 边界当初始界(−2³¹ 是合法值)。正确:nil 哨兵上下界传递,或中序 prev 前驱严格递增。

11 · BST 删除节点的三种情况?

后继顶替

叶子直删;单孩子上提;双孩子用中序后继(右子树最左)换值再删后继——后继最多带一个右孩子,删除必落入前两种简单情况。对称地也可用前驱。

12 · LCA 的递归怎么设计?BST 版能更快吗?

返回值=命中O(h) 走分支

一般二叉树:后序返回命中节点,左右都非空即 LCA,O(n)。BST 版利用有序:pq 都小于根往左、都大于往右、首次分叉即 LCA——O(h) 无需遍历全树。

13 · 直径(543)/最大路径和(124)的套路?

全局变量旁路

路径可"跨根"但递归只能返回"单边"——返回值给父用(高度),同时用全局变量更新"左+右"的跨根答案。两个题共用这个"返回值/答案分离"模式。

14 · 序列化怎么保证唯一还原?

null 占位

前序 + null 占位符:null 补齐"缺失的结构位",反序列化按同一游标消费即可唯一重建;层序+null 同理。不带 null 的裸前序无法区分结构(同一前序可对应多棵树)。

15 · 树的递归为什么常是"后序"?什么时候用前序?

汇总 vs 传递

需要孩子结果的(高度、直径、LCA、删除)→ 后序;需要祖先状态的(路径总和、构建BST、上下界)→ 前序带参数下行。中序几乎只在 BST 里用(有序性)。

后七题:建树必中序的原因、验证 BST 反例、删除三情况、LCA 与 BST 加速、跨根路径套路、序列化 null、三序的选择逻辑。第 15 题是"什么时候用什么序"的总结性答案,适合收尾。

Related & References

相关知识点与参考

数据结构系列(本分类)

堆与优先队列 →(完全树数组化的直接应用)
平衡树 →(BST 退化的解药:AVL/红黑/B+)
Trie →(多叉树的特化形态)
栈与队列 →(迭代遍历的载体)

算法系列

回溯 →(前序收集 + 撤销的一般化)
图算法 →(树是图的特例,DFS/BFS 推广)
复杂度分析 →(递归栈深 = 树高)
二分查找 →(BST 查找的数组版镜像)

参考来源(本 deck 结论可溯源至下列一手材料)

CLRS ch12 · Binary Search TreesBST 性质、插入删除(含后继替换)形式化
LeetCode 102/103/199/105/106/297/98/450/236/543 题解遍历变形、构造、验证、LCA 与路径套路
J. H. Morris · Traversing Binary Trees Simply and Cheaply (1979)Morris 线索化遍历原始论文
oi-wiki.org/ds/binary-tree性质推导与遍历模板(中文对照)
完全二叉树数组化父子公式堆实现标准结论(CLRS ch6 / 本仓库堆 deck 互勘)
收尾:四条数据结构链接(堆/平衡树/Trie/栈队列)+ 四条算法链接(回溯/图/复杂度/二分)。Morris 论文与 CLRS 章节给出学术出处,堆与平衡树两篇是本篇的直接后续。