04 二叉树与 BST
树题不要一题一题背,要先统一成“遍历 + 返回值”的思路。
一、先记住四种遍历
// 前序:中左右
// 中序:左中右
// 后序:左右中
// 层序:按层 BFS
如何选:
// 构造 / 路径 => 前序
// BST 有序性 => 中序
// 子树向上返回信息 => 后序
// 每层信息 => 层序
二、DFS 统一模板
def dfs(node):
if not node:
return base
left = dfs(node.left)
right = dfs(node.right)
// 组合左右子树的信息
return 给父节点的结果
关键问题:
// 当前题里,dfs(node) 到底表示什么?
// 是高度?路径和?是否合法?还是某个最优值?
三、重点题
1. Maximum Depth of Binary Tree
这个题型 / 算法点的总结
Maximum Depth of Binary Tree 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
题目要你求二叉树的最大深度,也就是从根节点到最深叶子节点一共经过多少层。最自然的定义就是:depth(root) = 1 + max(depth(left), depth(right))。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from typing import Optional
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right))
时间复杂度
O(n)。
空间复杂度
O(h),递归栈高度为树高。
怎么想到这个方法
树高题往往直接写递归定义最自然:当前节点的深度等于左右子树更深者加一。
示例 case
- 输入:一棵高度为 3 的二叉树
- 输出:
3。递归返回左右子树更深者加一。
常见 Follow-up
- BFS 版怎么写?
- 平衡树和链式树的空间区别是什么?
2. Invert Binary Tree
这个题型 / 算法点的总结
Invert Binary Tree 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
这题就是把每个节点的左右子树交换。因为每个节点都做同一件事,所以非常适合 DFS 或递归。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from typing import Optional
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:
return None
root.left, root.right = self.invertTree(root.right), self.invertTree(root.left)
return root
时间复杂度
O(n)。
空间复杂度
O(h)。
怎么想到这个方法
每个节点都做同样的左右交换,所以递归是最顺手的表达方式。把这题想成“后序地返回交换后的子树”就很自然。
示例 case
- 输入:二叉树根节点
- 输出:左右子树全部镜像交换后的树。
常见 Follow-up
- 迭代版怎么写?
- 为什么这题特别适合解释递归框架?
3. Binary Tree Inorder Traversal
这个题型 / 算法点的总结
Binary Tree Inorder Traversal 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
题目要求中序遍历二叉树,也就是按 left -> root -> right 的顺序输出节点值。面试里通常会优先写迭代版,因为它能体现你真的理解栈是怎么模拟递归的。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from typing import List, Optional
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
stack = []
ans = []
cur = root
while stack or cur:
while cur:
stack.append(cur)
cur = cur.left
cur = stack.pop()
ans.append(cur.val)
cur = cur.right
return ans
时间复杂度
O(n)。
空间复杂度
O(h)。
怎么想到这个方法
中序遍历的顺序固定是左根右。迭代版的重点是理解:一路向左压栈,相当于手动模拟递归调用栈。
示例 case
- 输入:
[1,null,2,3] - 输出:
[1,3,2]。中序遍历顺序是左、根、右。
常见 Follow-up
- 前序和后序的迭代写法如何改?
- 如果是 BST,中序遍历有什么额外性质?
4. Symmetric Tree
这个题型 / 算法点的总结
Symmetric Tree 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
对称树的关键不是分别判断左右子树,而是判断:
- 左树的左子树 vs 右树的右子树
- 左树的右子树 vs 右树的左子树
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from typing import Optional
class Solution:
def isSymmetric(self, root: Optional['TreeNode']) -> bool:
def mirror(a, b):
if not a and not b:
return True
if not a or not b or a.val != b.val:
return False
return mirror(a.left, b.right) and mirror(a.right, b.left)
return mirror(root.left, root.right) if root else True
时间复杂度
O(n)。
空间复杂度
O(h) 递归栈。
怎么想到这个方法
对称不是看单边,而是同时比较两棵镜像子树:左的左对右的右,左的右对右的左。
示例 case
- 输入:左右镜像的二叉树
- 输出:
True。镜像比较要同时看左左对右右、左右对右左。
常见 Follow-up
- 迭代版怎么用队列成对比较?
- 如果是
same tree,条件怎么变化?
5. Binary Tree Level Order Traversal
这个题型 / 算法点的总结
Binary Tree Level Order Traversal 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
这是最标准的树 BFS 题。
用队列一层一层处理,每次循环先记录当前层大小,然后把这一层所有节点弹出并把孩子加入队列。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from collections import deque
from typing import List, Optional
class Solution:
def levelOrder(self, root: Optional['TreeNode']) -> List[List[int]]:
if not root:
return []
ans = []
q = deque([root])
while q:
level = []
for _ in range(len(q)):
node = q.popleft()
level.append(node.val)
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
ans.append(level)
return ans
时间复杂度
O(n)。
空间复杂度
O(n)。
怎么想到这个方法
只要题目要求“按层输出”,那就是 BFS 队列模板。队列长度天然告诉你这一层有几个节点。
示例 case
- 输入:普通二叉树
- 输出:按层分组的节点值数组,例如
[[3],[9,20],[15,7]]。
常见 Follow-up
- 如果改成自底向上输出怎么做?
- DFS 能不能写出层序遍历?
6. Diameter of Binary Tree
这个题型 / 算法点的总结
Diameter of Binary Tree 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
对每个节点:
- 经过它的最长路径 = 左子树高度 + 右子树高度
- 返回给父节点的是自己的高度
所以这题是标准的“后序遍历 + 全局答案”。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from typing import Optional
class Solution:
def diameterOfBinaryTree(self, root: Optional['TreeNode']) -> int:
ans = 0
def depth(node):
nonlocal ans
if not node:
return 0
left = depth(node.left)
right = depth(node.right)
ans = max(ans, left + right)
return 1 + max(left, right)
depth(root)
return ans
时间复杂度
O(n)。
空间复杂度
O(h)。
怎么想到这个方法
树的直径题通常不是在每个点向外暴力扩,而是后序遍历时顺便拿到左右子树高度,再更新答案。
示例 case
- 输入:二叉树
- 输出:任意两节点间最长路径长度。答案不一定经过根。
常见 Follow-up
- 为什么答案不一定经过根?
- 如果要求输出路径本身,还要额外记录什么?
7. Validate Binary Search Tree
这个题型 / 算法点的总结
Validate Binary Search Tree 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
不能只检查:
node.left < node < node.right
因为 BST 的约束是整棵子树范围。
所以递归时要传入合法区间 (low, high)。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from typing import Optional
class Solution:
def isValidBST(self, root: Optional['TreeNode']) -> bool:
def valid(node, low, high):
if not node:
return True
if not (low < node.val < high):
return False
return valid(node.left, low, node.val) and valid(node.right, node.val, high)
return valid(root, float("-inf"), float("inf"))
时间复杂度
O(n)。
空间复杂度
O(h)。
怎么想到这个方法
BST 校验容易掉进“只比父节点”的坑。正确思路是给每个节点传可取值范围,或者用中序递增性质。
示例 case
- 输入:一棵二叉树
- 输出:判断是否满足 BST 全局有序约束,而不是只比较父子节点。
常见 Follow-up
- 为什么只比较父子节点不够?
- 中序遍历版怎么写?
8. Kth Smallest Element in a BST
这个题型 / 算法点的总结
Kth Smallest Element in a BST 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
BST 中序遍历结果是递增的。
所以只要中序遍历到第 k 个节点,返回它的值即可。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from typing import Optional
class Solution:
def kthSmallest(self, root: Optional['TreeNode'], k: int) -> int:
stack = []
while True:
while root:
stack.append(root)
root = root.left
root = stack.pop()
k -= 1
if k == 0:
return root.val
root = root.right
时间复杂度
O(h+k),最坏 O(n)。
空间复杂度
O(h)。
怎么想到这个方法
BST 的中序遍历天然有序,所以这题最直接的思路就是中序走到第 k 个就停。
示例 case
- 输入:BST 和
k = 3 - 输出:中序遍历第 3 个节点值。
常见 Follow-up
- 如果树频繁更新、频繁查询第 k 小,怎么优化?
- 如果要求第 k 大呢?
9. Lowest Common Ancestor of a Binary Tree
这个题型 / 算法点的总结
Lowest Common Ancestor of a Binary Tree 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
如果:
- 左子树找到了一个目标
- 右子树也找到了另一个目标
那么当前节点就是最近公共祖先。
否则,把找到的那个节点往上返回。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root or root == p or root == q:
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right:
return root
return left or right
时间复杂度
O(n)。
空间复杂度
O(h)。
怎么想到这个方法
LCA 的递归核心是:如果左右子树分别找到了不同目标,那么当前节点就是分叉点。
示例 case
- 输入:二叉树中的两个节点
- 输出:它们最近的公共祖先节点。
常见 Follow-up
- 如果是 BST,如何利用有序性优化?
- 如果节点有父指针,能不能换思路?
10. Binary Tree Right Side View
这个题型 / 算法点的总结
Binary Tree Right Side View 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
右视图的意思是:每一层只看最右边那个节点。
最直接做法是层序遍历,每层最后访问到的节点加入答案。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from collections import deque
from typing import List, Optional
class Solution:
def rightSideView(self, root: Optional['TreeNode']) -> List[int]:
if not root:
return []
ans = []
q = deque([root])
while q:
size = len(q)
for i in range(size):
node = q.popleft()
if node.left:
q.append(node.left)
if node.right:
q.append(node.right)
if i == size - 1:
ans.append(node.val)
return ans
时间复杂度
O(n)。
空间复杂度
O(n)。
怎么想到这个方法
题目按层看每层最右边节点,所以最自然还是层序遍历,只记录每层最后一个出队节点。
示例 case
- 输入:二叉树
- 输出:从右侧看到的节点列表,例如
[1,3,4]。
常见 Follow-up
- DFS 也能做吗?
- 如果要左视图,只改哪一行?
11. Path Sum III
这个题型 / 算法点的总结
Path Sum III 的核心是先识别它最像哪种经典题型,再把题目翻译成那个模板。
题目含义
这题和数组里的 Subarray Sum Equals K 很像。
我们维护从根到当前节点的前缀和 prefix。
如果之前出现过某个前缀和 prefix - targetSum,说明中间这一段路径和正好等于目标值。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from collections import defaultdict
from typing import Optional
class Solution:
def pathSum(self, root: Optional['TreeNode'], targetSum: int) -> int:
count = defaultdict(int)
count[0] = 1
def dfs(node, prefix):
if not node:
return 0
prefix += node.val
ans = count[prefix - targetSum]
count[prefix] += 1
ans += dfs(node.left, prefix)
ans += dfs(node.right, prefix)
count[prefix] -= 1
return ans
return dfs(root, 0)
时间复杂度
O(n)。
空间复杂度
O(n)。
怎么想到这个方法
这题虽然在树上,但问的是“路径和为 target 的条数”,所以要联想到数组里的前缀和计数,再把它搬到 DFS 路径上。
示例 case
- 输入:二叉树和
targetSum - 输出:所有向下路径中和等于目标值的条数。
常见 Follow-up
- 为什么不能只从根开始算?
- 如果路径必须从根到叶,思路怎么简化?
12. Flatten Binary Tree to Linked List
这个题型 / 算法点的总结
Flatten Binary Tree to Linked List` 主要在练链表指针操作,重点是想清楚节点之间该怎么断开、反转和接回去。
题目含义
目标是把树变成前序遍历顺序的链表。
对当前节点:
- 先拍平左右子树
- 如果有左子树,就把左子树插到右边
- 再把原右子树接到左子树尾部
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from typing import Optional
class Solution:
def flatten(self, root: Optional['TreeNode']) -> None:
def dfs(node):
if not node:
return None
left_tail = dfs(node.left)
right_tail = dfs(node.right)
if left_tail:
left_tail.right = node.right
node.right = node.left
node.left = None
return right_tail or left_tail or node
dfs(root)
时间复杂度
O(n)。
空间复杂度
O(h)。
怎么想到这个方法
题目要原地把树拉平成先序链表,所以你需要保住左右子树的连接关系。后序返回尾节点会写得更稳。
示例 case
- 输入:二叉树
- 输出:原地拉平成先序遍历顺序的右链表。
常见 Follow-up
- 你能讲 Morris 风格的
O(1)空间做法吗? - 为什么要先处理右子树再处理左子树?
13. Construct Binary Tree from Preorder and Inorder
这个题型 / 算法点的总结
Construct Binary Tree from Preorder and Inorder 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
关键性质:
- 前序第一个元素一定是根
- 中序中根左边是左子树,右边是右子树
所以每次递归:
- 从前序取根
- 在中序定位根
- 递归构建左右子树
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from typing import List, Optional
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional['TreeNode']:
pos = {value: i for i, value in enumerate(inorder)}
pre_idx = 0
def build(left, right):
nonlocal pre_idx
if left > right:
return None
root_val = preorder[pre_idx]
pre_idx += 1
root = TreeNode(root_val)
mid = pos[root_val]
root.left = build(left, mid - 1)
root.right = build(mid + 1, right)
return root
return build(0, len(inorder) - 1)
时间复杂度
O(n)。
空间复杂度
O(n)。
怎么想到这个方法
前序给根,中序帮你切左右子树。只要你能说清“根是谁、左右范围怎么切”,这题就已经过半了。
示例 case
- 输入:前序和中序遍历数组
- 输出:还原出的原始二叉树。
常见 Follow-up
- 为什么需要
value -> index哈希表? - 如果改成中序 + 后序呢?
14. Binary Tree Maximum Path Sum
这个题型 / 算法点的总结
Binary Tree Maximum Path Sum 主要在练树的递归返回值设计。做这类题时先决定遍历方式,再决定递归函数返回什么。
题目含义
这题最容易混淆的点是:
- 返回给父节点的值
- 全局最大路径值
不是一回事。
对每个节点:
- 向父节点返回:
node.val + max(left_gain, right_gain) - 更新全局答案:
node.val + left_gain + right_gain
负贡献要直接丢掉,所以要和 0 比较。
Python 代码
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
from typing import Optional
class Solution:
def maxPathSum(self, root: Optional['TreeNode']) -> int:
ans = float("-inf")
def dfs(node):
nonlocal ans
if not node:
return 0
left = max(dfs(node.left), 0)
right = max(dfs(node.right), 0)
ans = max(ans, node.val + left + right)
return node.val + max(left, right)
dfs(root)
return ans
时间复杂度
O(n)。
空间复杂度
O(h)。
怎么想到这个方法
最大路径和的难点是区分“往上返回给父节点的值”和“当前节点内部更新答案的值”。这通常是树 DP 的标志。
示例 case
- 输入:可能包含负数的二叉树
- 输出:任意路径的最大路径和。
常见 Follow-up
- 为什么返回值不能同时带左右两边?
- 如果节点值全负,初始化要注意什么?