本篇题目
常见二叉树类型与性质
Complete Binary Tree 完全二叉树
- 每层从左到右填满,最后一层可以不满但必须靠左
- 编号规律(从 1 开始):左孩子 = 父 × 2,右孩子 = 父 × 2 + 1
- 堆(heap)就是用数组实现的完全二叉树,利用的正是这套下标规律
IMPORTANT完全二叉树编号规律是 LeetCode 662 的核心
不管实际树长什么样,按完全二叉树规则给节点编号:左孩子 = 父 × 2,右孩子 = 父 × 2 + 1。同一层宽度 = 最右编号 − 最左编号 + 1,中间的空节点自动被计入。
1 / \ 2 3 / \ / 4 5 6
最后一层靠左排列,是完全二叉树 ✓Perfect Binary Tree 满二叉树(中文叫法)
- 每一层全部填满,节点总数恰好是 2ⁿ - 1
1 / \ 2 3 / \ / \ 4 5 6 7
每层全满,节点数 = 2³ - 1 = 7 ✓TIP中英文差异
英文 Full Binary Tree ≠ 中文满二叉树。Full 在英文里指每个节点要么 0 个孩子要么 2 个孩子;中文满二叉树对应的英文是 Perfect Binary Tree。看英文资料时留意。
Binary Search Tree (BST) 二叉搜索树
- 左子树所有节点 < 根,右子树所有节点 > 根,每棵子树也满足此性质
- 查找、插入、删除平均 O(log n),最坏退化成链表 O(n)
5 / \ 3 8 / \ / \ 1 4 7 9
中序遍历:1 3 4 5 7 8 9 ← 严格升序 ✓TIP刷题常用结论
BST 的中序遍历结果是升序序列,遇到”验证 BST”或”第 K 小元素”类题目优先想中序遍历。
Balanced Binary Tree 平衡二叉树
- 任意节点的左右子树高度差不超过 1
- 常见实现:AVL 树、红黑树
3 3 / \ \ 2 4 4 / \ 1 5 \ 6
左边:高度差最大为 1,是平衡树 ✓右边:右侧一直延伸,退化成链表 ✗TIP刷题写法
判断是否平衡用后序遍历,返回子树高度,一旦高度差 > 1 就返回 -1 向上传递”已不平衡”的信号,避免重复计算。
LeetCode 662 · 二叉树最大宽度
思路
普通 BFS 无法计算宽度,因为中间的空节点不在队列里,数不出来。
关键想法:借用完全二叉树的编号规则给每个节点编号。不管实际树长什么样,都按完全二叉树的规则假设编号存在。这样同一层最右节点编号 - 最左节点编号 + 1 就是这层宽度,中间跳过的空位自动被算进去。
代码
class Solution: def widthOfBinaryTree(self, root: Optional[TreeNode]) -> int: if not root: return 0
# 记录全局最大宽度 ans = 0 # 队列存 (编号, 节点),根节点编号为 1 # 编号规则和完全二叉树一样:左孩子=父*2,右孩子=父*2+1 queue = deque([(1, root)])
while queue: # 每轮处理一整层 # 初始化为正无穷,保证第一个 code 一定能更新 min_seen min_seen = inf # 初始化为 0,保证第一个 code 一定能更新 max_seen max_seen = 0
# len(queue) 是当前层的节点数,循环只处理这一层 for i in range(len(queue)): code, node = queue.popleft()
# 把下一层的子节点连同编号一起入队 if node.left: queue.append((code * 2, node.left)) if node.right: queue.append((code * 2 + 1, node.right))
# 更新本层出现过的最小编号(最左节点) min_seen = min(code, min_seen) # 更新本层出现过的最大编号(最右节点) max_seen = max(code, max_seen)
# 本层宽度 = 最右编号 - 最左编号 + 1 # 中间跳过的空节点因为编号连续,自动被算进去了 ans = max(ans, max_seen - min_seen + 1)
return ans总结
- 核心技巧:把额外信息(编号)打包进元组和节点一起入队,是 BFS 题的常见写法
- 复杂度:时间 O(n),空间 O(n)(队列最多存一层节点)
TIP编号溢出问题
树很深时编号指数级增长,可以在每层开始时把所有编号减去本层
min_seen归一化,只保留相对差值。Python 整数不会溢出,但 Java / C++ 需要特别注意。
TIP
inf和0的初始化技巧找最小值初始化为正无穷
inf,找最大值初始化为0(或负无穷),保证第一个真实值一定能更新它,不需要额外判断”是否是第一个元素”。
LeetCode 111 · 二叉树的最小深度
思路
BFS 按层遍历,第一次遇到叶子节点时,当前层数就是最小深度,直接返回。
TIP注意坑
最小深度不是单纯找最短路径,必须到达叶子节点(左右都为空)才算终点。如果用递归直接取
min(左深度, 右深度),当某侧子树为空时会返回 0,导致结果偏小。BFS 天然规避了这个问题,遇到叶子才返回,不会被空子树干扰。1/2递归错误写法:min(dfs(左)=1, dfs(右)=0) = 0,答案错误BFS 正确:第 2 层遇到叶子节点 2,返回深度 2 ✓
代码
from collections import deque
class Solution: def minDepth(self, root: Optional[TreeNode]) -> int: if not root: return 0
queue = deque([root]) # 队列只存节点,不需要带额外信息 depth = 0 # 记录当前层数
while queue: depth += 1 # 进入新的一层,深度 +1
for _ in range(len(queue)): # 处理当前层所有节点 node = queue.popleft()
# 把下一层的子节点入队 if node.left: queue.append(node.left) if node.right: queue.append(node.right)
# 遇到叶子节点,当前 depth 就是最小深度,直接返回 if not node.left and not node.right: return depth
return depth总结
- 核心技巧:BFS 天然按层遍历,第一个叶子节点一定是最浅的,直接返回无需比较
- 复杂度:时间 O(n),空间 O(n)(最坏情况队列存满一层)
BFS 通用模板
from collections import deque
def bfs(root): if not root: return 0 # 1. 边界处理
queue = deque([root]) # 2. 初始化队列(根据题意决定队列里存什么) res = ... # 3. 初始化返回值
while queue: # 4. 外层循环:队列不空就继续 layer_var = ... # 每层开始前初始化层级变量
for _ in range(len(queue)): # 5. 内层循环:处理当前层所有节点 node = queue.popleft()
if node.left: queue.append(node.left) # 6. 下一层入队 if node.right: queue.append(node.right)
# 7. 处理当前节点(更新层级变量,或提前 return) ...
# 8. for 结束 = 这层处理完,更新返回值 res = ...
return res # 9. 返回结果IMPORTANTBFS 通用模板核心要点
外层
while queue控制层数,内层for _ in range(len(queue))快照当前层节点数。队列里存什么、每层初始化什么、何时提前 return——这三点决定了模板怎么变形。
两题对比
| 步骤 | 662 最大宽度 | 111 最小深度 |
|---|---|---|
| 队列存的内容 | (编号, 节点) | 只存节点 |
| 每层初始化 | min_seen = inf, max_seen = 0 | 无,depth += 1 直接加 |
| 处理节点 | 更新 min_seen / max_seen | 检查是否叶子节点 |
| 每层结束后 | ans = max(ans, max_seen - min_seen + 1) | 遇叶子提前 return,否则继续 |
TIP看到什么题用 BFS
- 关键词含层、深度、宽度、距离、最近、最短
- 需要逐层处理,或者找到第一个满足条件的节点就返回
- 题目涉及从根往下扩散的过程
TIP队列里存什么
默认只存节点。如果需要额外信息(编号、步数、路径等),就把信息和节点打包成元组一起入队,弹出时解包使用。
TIP提前 return vs 处理完再 return
- 找最小/最近:在 for 循环内遇到条件立刻
return,不用等这层全部处理完- 找最大/统计全部:等 for 循环结束(整层处理完)再更新结果