【LC 104 / 226】二叉树的最大深度 & 翻转二叉树:两道送分题里各藏着一颗钉子
适用读者:刚学完二叉树递归,想知道"我到底有没有真懂"的人
技术栈:Python 为主,附 Java 版
难度:简单 —— 但两道题的正确率都没那么高,原因在下面
收获:把「树的递归」这件事拆成两种模式(后序汇报 vs 就地改结构),顺带打穿 111 / 110 / 543 / 101 / 951 一整串变体
题目
这两道题放在一起讲,因为它们正好是
树递归的两种模式,而且边界情况会互相照应。
LC 104 二叉树的最大深度
给定一个二叉树
root,返回其
最大深度。最大深度是指从根节点到最远叶子节点的最长路径上的节点数。
输入:root = [3,9,20,null,null,15,7]
输出:3
输入:root = [1,null,2]
输出:2
3 ← 第 1 层
/ \
9 20 ← 第 2 层
/ \
15 7 ← 第 3 层
LC 226 翻转二叉树
给你一棵二叉树的根节点
root,翻转这棵二叉树,并返回其根节点。
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
4 4
/ \ / \
2 7 → 7 2
/ \ / \ / \ / \
1 3 6 9 9 6 3 1
对,就是那个传说中 Homebrew 作者写不出来、被 Google 拒了的题。
节点的定义,沿用前几篇:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
输入格式提醒:[3,9,20,null,null,15,7] 是 LeetCode 的层序数组编码(和 LC 102 / 236 / 124 一样)。下标 i 的节点,左孩子在 2i+1、右孩子在 2i+2,空缺位置用 null 占位。所以上面这棵树里 9 的两个孩子位是 null(它是叶子),[15, 7] 挂在 20 下面。
一、这两题为什么放在一起
因为它们的形式几乎一样(都是三五行递归),但
"递归顺序"的重要性恰好相反:
一句话概括这个对照:
104 是"信息从下往上冒",226 是"从上往下挨个改"。
前者的顺序是被迫的(拿不到子树答案就没法算自己);后者的顺序是自由的(因为每个节点的活儿互不相干)—— 而这恰恰是 226 唯一的坑所在。
二、LC 104:那个 1 不能少
出处备注:LC 104 就是美团面经里那道「求二叉树的高度」。它被反复问,不是因为难,而是因为它足够短 —— 面试官想看的不是你能不能写出来(人人都会),而是你能不能把「为什么必须是后序」「那个 1 代表什么」「会不会爆栈」这三件事讲清楚。
2.1 三行代码
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if not root:
return 0
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
短到没什么可讲的 —— 除了那个
1。
2.2 1 是什么:把自己这一层算上
max(self.maxDepth(root.left), self.maxDepth(root.right)) 给出的是
子树的高度,也就是"从孩子往下最多还能走几层"。但是题目要的是"从
当前节点 往下最多能走几层" —— 差的就是自己这一层。
3 maxDepth(9) = 1 ← 9 自己往下只有 1 层
/ \ maxDepth(20) = 2 ← 20 → 15(或 7)是 2 层
9 20 max(1, 2) = 2
/ \ 1 + 2 = 3 ← 加上"3"自己这一层
15 7
如果漏掉那个 1,全树都返回 0。 这是个"写出来一眼能发现"的错误,但在递归题里,同类错误(返回值的语义差一层)非常常见 —— LC 543 直径的"边数 vs 节点数"就是这个坑的升级版。
2.3 为什么必须后序
因为"子树高度"是
从下面冒上来的信息,父节点手里没有,只能先下去要。
硬要用先序(自顶向下)也能写:
def maxDepth(self, root):
self.ans = 0
def dfs(node, depth): # 把"当前深度"当参数传下去
if not node: return
depth += 1
self.ans = max(self.ans, depth)
dfs(node.left, depth)
dfs(node.right, depth)
dfs(root, 0)
return self.ans
能过,但绕了一圈,而且
拿不到"子树高度"这个中间量 —— 后面 LC 110 平衡二叉树那种"顺便判断左右子树高度差"的需求就做不了了。所以:
凡是"父节点的答案依赖子树答案"的题,后序几乎是唯一选择。
凡是"需要沿途累加一个参数"的题,先序更自然。
这句话值得记住。它区分了树递归的两大类,比背模板管用。
2.4 另一种写法:BFS 数层数
深度还有一个完全不用递归的算法 —— 层序遍历,每走完一层就 +1:
from collections import deque
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if not root:
return 0
q = deque([root])
depth = 0
while q:
depth += 1
for _ in range(len(q)): # ← 关键:先锁住这一层的节点数(LC 102 的坑)
node = q.popleft()
if node.left: q.append(node.left)
if node.right: q.append(node.right)
return depth
什么时候选 BFS? 当树
很深而递归会爆栈的时候。Python 默认递归深度限制是 1000,一棵 3 万节点的偏斜树用递归必挂;BFS 没有这个问题。
代价:BFS 的空间是 O(树的最大宽度),最坏 O(n/2);而 DFS 递归是 O(树高),平衡树上是 O(log n)。所以两者各有胜场,不是"哪个更高级"。
这也正好呼应 LC 102:那个题的 size = len(queue) 快照,在这里又用了一次(for _ in range(len(q)))。不锁住长度,内层一边出队一边入队,层就串了。
三、动画:看着高度一层层往上冒
下面这个动画把上面 11 帧推演过程逐帧画出来。图里:
- 节点上方挂的
h=2 = 这个节点算出来的高度(浅绿色边表示已经算完,橙色表示"这一帧刚算出来");
- 橙色粗边 = 当前递归正在走的那条路径(一眼看出"钻到哪一层了");
- 橙色粗圈 = 当前聚焦的节点;
- 底部横幅左边是这一帧的计算式,右边是最终答案 —— 注意它在根算完之前一直是
?。
看的时候重点盯三处:
- 第 3 帧:
9 是叶子,报出 h=1 —— 高度是从叶子开始往上冒的;
- 第 9 帧:
20 拿到左右各 1,算出 1 + max(1, 1) = 2;
- 第 10 帧:根
3 算出 1 + max(1, 2) = 3,答案 ? 在这一刻才变成 3。
第 3 点很值得玩味:
在根算完之前,没有任何一帧能说出最终答案。对比 LC 124 里的
best —— 那个是"一路取最大值"、中途不断被刷新,是完全相反的节奏。
四、LC 104 完整推演表(11 帧逐帧)
下表是动画每一帧的真实数据(
左 /
右 是子树回报的高度,空子树记
0):
两行必须对照着看:
第 8 帧(20) —— 左右都是
1,取
max 得 1,再加自己:
第 9 帧(根 3) —— 左右
不一样(1 和 2),这时候
max 才有意义:
h = 1 + max(1, 2) = 3 ← 取高的那 2 条,再加自己
如果这棵树左右一样高,就看不出"取大的"这个动作了。官方例专门挑了
9(叶子)和
20(有两层)作对比,就是为了让
max 的取舍看得见。
第 10 帧收尾时栈已清空,
h=3 从根报出来 —— 这就是答案。
五、LC 104 的三个变体
5.1 LC 111 最小深度:把 min 直接套上去就错了 ⚠️
这是 104 最重要的一节。 很多人做完 104,看到"最小深度"就顺手把
max 改成
min:
# ❌ 错的
def minDepth(self, root):
if not root: return 0
return 1 + min(self.minDepth(root.left), self.minDepth(root.right))
这行代码对大多数测试用例都能过,但遇到"只有一侧孩子"的树就崩:
minDepth(2) = 1
minDepth(null) = 0 ← 问题就在这
1 + min(1, 0) = 1 ← 错答 1
正确答案是
2(路径是
1 → 2,两个节点)。
为什么错? 因为
空子树不是叶子。
题目说的是"根节点到
最近叶子节点的最短路径"。而
null 不是一个节点,它
代表"这条路根本不存在",不能拿来参与
min 比较 —— 它给出的那个 0 会伪造出一条"长度为 0 的假路径"。
注意
max 为什么不会犯这个错:
max(1, 0) = 1 恰好把 0 淘汰了。所以
同样一个"空子树记 0"的约定位,在 max 下无害、在 min 下致命。
正确写法是
先把单侧空的情况摘出来:
class Solution:
def minDepth(self, root: TreeNode) -> int:
if not root:
return 0
# 只有右子树:必须往右走,不能把空的左边当成"更短的路径"
if not root.left:
return 1 + self.minDepth(root.right)
# 只有左子树:同理
if not root.right:
return 1 + self.minDepth(root.left)
# 两边都在,这时候 min 才有意义
return 1 + min(self.minDepth(root.left), self.minDepth(root.right))
这四行的语义是:
"最小深度只统计到真正的叶子为止,半路的内部节点不是终点。"
面试时如果只给你一道题,很可能是 111 而不是 104 —— 因为 104 写不错,111 才筛人。
5.2 LC 110 平衡二叉树:返回值兼作状态
LC 110:判断一棵二叉树是否是
高度平衡的(每个节点的左右两个子树的高度差的绝对值不超过 1)。
这题必须用后序,而且正好是 104 的骨架直接扩展:
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
def height(node: TreeNode) -> int:
"""返回子树高度;一旦发现不平衡就返回 -1(哨兵)"""
if not node:
return 0
lh = height(node.left)
if lh == -1: # ← 提前剪枝:左边已经不平衡,不用再看右边
return -1
rh = height(node.right)
if rh == -1:
return -1
if abs(lh - rh) > 1: # ← 就是这一步在"顺便判断"
return -1
return 1 + max(lh, rh) # ← 104 的原式
return height(root) != -1
这题的设计很值得学:返回值本来是"高度"(一个非负数),这里
借用 -1 表示"已经不满足了" —— 一个返回值同时承载了"数值"和"状态"两种信息。
这和 LC 236 里"返回值有三种身份"、LC 124 里"返回值只管一条胳膊"是同一类技巧:
返回值的语义可以比"它字面上是什么"更丰富。 树形递归的题,难点十有八九在这里。
时间复杂度 O(n)(每个节点访问一次),空间 O(h)。
5.3 LC 543 二叉树的直径
这个在 LC 124 那篇已经详细讲过,一句话带过:
同一个"两条胳膊"的骨架,把节点值之和换成边数,而且不需要 clamp(深度不可能为负)。
六、LC 226 翻转二叉树
6.1 先说这段著名的公案
2015 年 6 月,Homebrew 的作者
Max Howell 去 Google 面试 iOS 岗。他后来在 Twitter 上说:
Google: 90% of our engineers use the software you wrote (Homebrew), but you can't invert a binary tree on a whiteboard, so …(原话结尾是一个脏字。)
(大意:Google 说我们 90% 的工程师都在用你写的 Homebrew,但你没法在白板上翻转一棵二叉树,所以你还是滚吧。)
这条推文炸了。Google 自己的工程师 Randy Luecke 在下面问"我们真的问了你那个问题?",
Jonathan Blow 则表示"翻转二叉树是很基础的数据结构题,答不出来我也一样不录用"。Howell 后来也补充说过:当时收到的反馈里其实还有多项弱点,那条推文是为了传播效果简化过的。
这段公案值得放在这里,是因为它精准地展示了这道题的"尴尬":
它只有五行,没有任何算法洞察,会二叉树和递归的人两分钟就能写完。
但它确实在筛人 —— 筛的是"你对递归这件事熟不熟到不需要思考"。
6.2 五行代码
class Solution:
def invertTree(self, root: TreeNode) -> TreeNode:
if not root:
return None
root.left, root.right = root.right, root.left # 换掉自己的两个孩子
self.invertTree(root.left) # 递归左边
self.invertTree(root.right) # 递归右边
return root
这题真正在考什么? 一句话:
遍历每个节点,把它的两个孩子交换一次。
就这么简单。想清楚这一句,代码自然就有了 —— 甚至连"什么时候交换"都不需要纠结,因为:
6.3 顺序真的无所谓 —— 但有一个位置是雷
前序写法(先换自己,再递归两个孩子):
root.left, root.right = root.right, root.left # 先换
self.invertTree(root.left) # 递归的是"交换后"的左
self.invertTree(root.right)
后序写法(先递归两个孩子,再换自己):
self.invertTree(root.left) # 递归的是"交换前"的左
self.invertTree(root.right)
root.left, root.right = root.right, root.left # 后换
两个都对。 因为每个节点的操作是"换自己的两个孩子",这件事
不依赖子树的状态,也不改变"有哪些节点",所以谁先谁后都不干涉。
但是下面这种就一定错:
# ❌ 把交换插在两个递归中间
self.invertTree(root.left) # 先把左子树翻好了
root.left, root.right = root.right, root.left # 再交换 —— 左子树白翻了!
self.invertTree(root.right) # 这里进的是"从没翻过的"那棵
问题出在
变量 root.left 指向的东西变了:
- 第一行递归的是原来的左子树,它翻好了;
- 第二行一交换,这个"翻好的左子树"跑到右边去了;
- 第三行
root.right 现在指的是翻好的那棵 —— 于是它被翻了第二次(等于白翻);
- 而真正的右子树一次都没被翻。
结果:左右两棵子树各自的状态完全错乱。
规则:交换要么放在两次递归之前,要么放在之后,绝不能夹在中间。
根因是"交换会改变 root.left / root.right 的指向",而递归调用又依赖这两个引用 —— 用变了引用的变量去递归,语义就崩了。
6.4 一个漂亮的等价说法:路径逐位取反
翻转二叉树还有一个极优雅的等价描述 ——
把每个节点从根到它的路径逐位取反(左变右、右变左):
节点 原路径 翻转后
2 [L] → [R]
7 [R] → [L]
1 [L,L] → [R,R]
3 [L,R] → [R,L]
6 [R,L] → [L,R]
9 [R,R] → [L,L]
本文的动画就是按这个等价说法做的 —— 状态机不存"节点该画在哪",只存每个节点相对父亲的左右角色;位置由"沿 parent 链拼出路径 → 算列号"推出来。所以交换一次就是改一个角色的值,节点的横坐标自然跳到镜子里的位置。
附带一个漂亮的推论:
翻转是自逆运算 —— 翻两次就还原了。因为"逐位取反"做两次就是原值。
七、动画:看着树真的翻过去
下面这个动画的关键在于 ——
节点是真的会换位置的。这和系列里其它树动画不一样(那些树都是静态的,只有颜色在变):
- 橙色虚线弧 + 双向箭头 = 这一帧正在互换的两个孩子;
- 橙色粗圈 = 当前聚焦的节点;橙色粗边 = 当前递归路径;
- 淡绿填充的节点 = 已经翻转过的(它的两个孩子已经交换);
- 右下角进度条实时显示
已换 x/y 个非叶节点。
看的时候重点盯三处:
- 第 2 帧:
4 交换它的两个孩子 —— 2 和 7 对调,两棵子树整体跟着挪过去,这是"翻"这件事最直观的一刻;
- 第 4 帧:交换
7 的孩子时,只动 6 和 9,7 的位置不变 —— 因为每一层的交换只管自己这一层;
- 第 12 帧:全部节点都变绿,整棵树成了原树的镜像。
顺带注意动画里的
叶子节点也会被"访问"(第 5、7、9、11 帧):它们的孩子都是
null,
交换两个 null 是空操作 —— 所以真正发生变化的只有非叶节点。进度条上的分母
3 就是这个意思。
八、LC 226 完整推演表(12 帧逐帧)
下表是动画每一帧的真实数据(用的是前序写法:
先交换自己,再递归):
两个容易看错的点:
第 2 帧和第 7 帧的"进哪边" —— 前序写法里,递归左孩子时,进的是
交换后的左孩子(也就是原来的右孩子):
第 1 帧:4 交换 → 左孩子从 2 变成 7
第 2 帧:递归进"左孩子",进的是 7,不是 2
所以动画里的访问顺序是
4 → 7 → 9 → 6 → 2 → 3 → 1,看起来"先右后左",其实严格是"先左后右" —— 只是左右已经被换过了。
这也是那段"交换不能夹在两个递归中间"的坑的另一种表现:只要你在递归里用
root.left,就必须清楚它此刻指的是谁。
反过来,如果写成后序(先递归再交换),访问顺序会变成
1 → 3 → 2 → 6 → 9 → 7 → 4,
但从下往上换,最终结果完全一样。你可以自己改成后序跑一遍感受一下 —— 这正是"顺序无所谓"的实证。
九、LC 226 的两个变体
9.1 LC 101 对称二叉树:本质就是"自己等于翻转后的自己"
LC 101:判断一棵二叉树是否
轴对称。
想清楚它和 226 的关系:一棵树轴对称,等价于"
它翻转之后和自己长得一样"。所以判据是
镜像递归 —— 同时拿两棵子树往下走,一边往左、一边往右:
class Solution:
def isSymmetric(self, root: TreeNode) -> bool:
def mirror(a: TreeNode, b: TreeNode) -> bool:
if not a and not b:
return True # 都空 → 这一路对上
if not a or not b:
return False # 一空一不空 → 对不上
if a.val != b.val:
return False # 值不一样 → 对不上
# 关键:比较的是"a 的左 ↔ b 的右"和"a 的右 ↔ b 的左"
return mirror(a.left, b.right) and mirror(a.right, b.left)
return mirror(root, root) if root else True
注意那个 mirror(a.left, b.right) and mirror(a.right, b.left) —— 交叉比较才是"镜像"。写成
mirror(a.left, b.left) 就变成判断"两棵树完全相同"(那是 LC 100)了。
和 226 的关系:226 是
动手翻,101 是
判断能不能翻成一样。一个改结构,一个只比较。
9.2 LC 951 等价翻转二叉树
LC 951:判断两棵树能否
通过若干次翻转左右孩子变得相同。
同一个镜像思路,但多一个分叉 ——
每个节点可以选择"翻"或者"不翻",两种都要试:
class Solution:
def flipEquiv(self, root1: TreeNode, root2: TreeNode) -> bool:
if not root1 and not root2:
return True
if not root1 or not root2:
return False
if root1.val != root2.val:
return False
# 要么两边都不翻,要么两边都翻 —— 各试一次
return (self.flipEquiv(root1.left, root2.left) and self.flipEquiv(root1.right, root2.right)) or \
(self.flipEquiv(root1.left, root2.right) and self.flipEquiv(root1.right, root2.left))
和 101 对比一下就明白了:
101 是"必须对称"(只有交叉那一支),951 是"翻或不翻都行"(两支都要试)。
9.3 变体关系总表
一个提醒:101 / 951 的形状和 226 明显不同(前者是
双指针同步下潜,后者是
单指针就地修改),所以本文没有给动画开
mode 硬塞进去 —— 那会误导人以为它们能共用一套代码。
形似的题可以合并讲,形不似的题要分开写。
十、Java 版
// LC 104
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
}
// LC 111 —— 注意四个分支
class Solution {
public int minDepth(TreeNode root) {
if (root == null) return 0;
if (root.left == null) return 1 + minDepth(root.right);
if (root.right == null) return 1 + minDepth(root.left);
return 1 + Math.min(minDepth(root.left), minDepth(root.right));
}
}
// LC 226
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) return null;
TreeNode tmp = root.left; // Java 不能像 Python 那样一行交换,得借临时变量
root.left = root.right;
root.right = tmp;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
Java 的两个注意点:
- 交换要借临时变量 —— Python 的
a, b = b, a 是元组解包,Java 没有对应语法,直接 root.left = root.right; root.right = root.left; 会把两边都变成同一个,是极高频的错误。
invertTree 的返回类型是 TreeNode 而不是 void —— 题目要求返回根节点。递归调用时可以忽略返回值(因为结构是就地改的),但外层必须 return root,方便链式调用和空树情形。
十一、边界情况与常见坑
边界情况(两题都要过一遍)
104 和 226 都不受节点值影响 —— 这是它们比 124(要考虑负数剪枝)简单的地方。
常见坑(编号对照上面各节)
坑 1:104 漏掉那个 1(§2.2)
return max(...) # ← 漏了 1,全树返回 0
坑 2:111 直接套 min(§5.1)⚠️ 最容易错
[1,2] 会错答
1。
空子树不是叶子,不能参与 min。
坑 3:110 忘记剪枝
发现不平衡后应该立刻返回
-1 并
跳过剩余子树,否则复杂度会退化(虽然仍是 O(n),但常数大很多),而且代码更难读。
坑 4:226 把交换夹在两个递归中间(§6.3)⚠️ 最隐蔽
代码能跑、小样例可能还对,但结果错乱。
根因是 root.left 的指向被交换改了。
坑 5:226 忘记返回 root
def invertTree(self, root):
if not root: return None
root.left, root.right = root.right, root.left
self.invertTree(root.left)
self.invertTree(root.right)
# ← 忘了 return root,函数返回 None
坑 6:101 的镜像比较写成非交叉
mirror(a.left, b.left) 是"两棵树相同"(LC 100),不是"镜像"。必须交叉。
坑 7:递归爆栈
深度 3 万+ 的偏斜树,Python 默认递归上限 1000,会
RecursionError。可以调高上限(
sys.setrecursionlimit(1000000)),或者 104 直接用 BFS(§2.4),226 改成显式栈的迭代写法。
坑 8:把"节点数"和"边数"搞混
104 返回的是
节点数(根到最远叶子的节点个数)。543 直径返回的是
边数。两者的差别正好是 1 —— 这就是 543 里
return 1 + max(...) 那一步的存在理由。
十二、面试怎么答
104 最大深度
第一句就把两个要点说出来:
"后序递归。返回 1 + max(左子树高度, 右子树高度),那个 1 是把自己这一层算上。"
如果面试官追问"能不能用别的顺序":
"先序也能做,但要额外传一个 depth 参数自顶向下累加。不过那样就拿不到'子树高度'这个中间量了 —— 110 平衡二叉树需要这个量,所以后序是更通用的选择。"
如果追问"会不会爆栈":
"会。深偏斜树会爆 Python 的默认递归上限。可以改成层序遍历(BFS)数层数,空间换 O(树宽),或者显式栈。"
然后主动提 111(这是加分项):
"顺便说一句,111 最小深度不能把 max 换成 min —— 空子树会被当成深度 0,伪造出一条假路径。[1,2] 这种只有一侧孩子的树会错答 1,正确是 2。得先把单侧空的情况单独摘出来处理。"
226 翻转二叉树
先把核心一句话说出来:
"本质就是遍历每个节点,把它的两个孩子交换一次。"
然后主动讲顺序(这是这道题唯一的考点):
"所以遍历顺序其实无所谓 —— 前序(先换再递归)和后序(先递归再换)都对,因为每个节点是独立地换自己的两个孩子。但交换不能夹在两个递归中间 —— 那样会让已经翻好的子树白翻一遍,因为这个引用被交换改掉了。"
如果面试官问"怎么证明顺序无所谓":
"因为交换操作是就地修改、不依赖子树状态;而每个节点又恰好被访问一次。两者交换顺序只会改变'交换发生的时刻',不会改变'每个节点被交换了一次'这个事实。"
如果被问"翻两次会怎样":
"还原。翻转等价于把每个节点的路径逐位取反(左变右、右变左),取反两次就是原值。所以这是个自逆运算。"
两题连着问时的收尾
"这两题正好是树递归的两种模式:104 的答案依赖子树返回值,所以顺序被迫后序;226 每个节点独立操作,所以顺序自由。判断一道树题该用哪种顺序,看'父节点的答案是否依赖子树'就够了。"
结语
104 和 226 都是 LeetCode 上的 easy,通常被视为"树的入门题"。但它们各自藏着一颗钉子:
- 104 的钉子长在 111 上 —— "空子树不是叶子"这句话,很多人做了几十道树题都没真正分清。
max 侥幸掩盖了它,min 一下就露馅。
- 226 的钉子长在"交换的位置"上 —— 五行代码里,只有"夹在两个递归中间"这一种写法是错的,而它错得很隐蔽。
如果要从这两题里带走一句最有用的东西,我选这句:
树形递归里最值钱的判断是:"父节点的答案,是不是依赖子树的返回值?"
依赖 → 后序(104 / 110 / 111 / 236 / 124)。
不依赖 → 顺序自由(226 / 102 / 100)。
想清楚这一点,绝大多数树题都能在两分钟内定下骨架 —— 剩下的就只是边界情况了。
系列导航:本文是「暑期面经汇总」算法系列的第 13 篇。前作:LC 206 反转链表 · LC 21 合并两个有序链表 · LC 23 合并 K 个升序链表 · LC 141/142 环形链表 · LC 19 删除链表倒数第 N 个节点 · LC 143 重排链表 · LC 234 回文链表 · LC 160 相交链表 · LC 82 删除排序链表中的重复元素 II · LC 102 二叉树的层序遍历 · LC 236 二叉树的最近公共祖先 · LC 124 二叉树中的最大路径和。