1. 项目概述
今天要分享的是代码随想录算法训练营第十八天的三道经典二叉树题目:LeetCode 530(二叉搜索树的最小绝对差)、501(二叉搜索树中的众数)和236(二叉树的最近公共祖先)。这三道题涵盖了二叉搜索树和普通二叉树的核心操作技巧,是面试中高频出现的题型。
作为算法训练营的常规练习,这三道题目的共同特点是都需要深入理解树结构的遍历方式,并灵活运用递归或迭代的解题思路。其中530和501题专门针对二叉搜索树(BST)的特性设计,而236题则考察对普通二叉树结构的理解。
2. 核心题目解析
2.1 LeetCode 530:二叉搜索树的最小绝对差
这道题要求我们找出二叉搜索树中任意两节点值之差的最小绝对值。BST的中序遍历结果是一个有序数组,这是解题的关键。
最优解法思路:
- 利用BST中序遍历有序的特性
- 在遍历过程中记录前驱节点
- 实时计算当前节点与前驱节点的差值
- 维护一个全局最小差值变量
python复制class Solution:
def getMinimumDifference(self, root: TreeNode) -> int:
self.prev = None
self.min_diff = float('inf')
def inorder(node):
if not node:
return
inorder(node.left)
if self.prev is not None:
self.min_diff = min(self.min_diff, node.val - self.prev)
self.prev = node.val
inorder(node.right)
inorder(root)
return self.min_diff
时间复杂度分析:
- 时间复杂度:O(n),需要遍历所有节点
- 空间复杂度:O(h),递归栈的深度取决于树的高度
2.2 LeetCode 501:二叉搜索树中的众数
这道题要求找出BST中出现次数最多的元素(可能不止一个)。BST的中序遍历有序性同样能帮助我们高效解决这个问题。
解题思路:
- 中序遍历BST,统计连续相同值的出现次数
- 维护当前最大值和结果列表
- 当发现更高频次时,清空结果列表并更新最大值
- 当频次等于最大值时,将当前值加入结果列表
python复制class Solution:
def findMode(self, root: TreeNode) -> List[int]:
self.current_val = None
self.current_count = 0
self.max_count = 0
self.result = []
def inorder(node):
if not node:
return
inorder(node.left)
# 处理当前节点
if node.val == self.current_val:
self.current_count += 1
else:
self.current_val = node.val
self.current_count = 1
# 更新结果
if self.current_count > self.max_count:
self.max_count = self.current_count
self.result = [self.current_val]
elif self.current_count == self.max_count:
self.result.append(self.current_val)
inorder(node.right)
inorder(root)
return self.result
优化技巧:
- 可以先用Morris遍历实现O(1)空间复杂度的解法
- 对于普通二叉树,可以先遍历统计所有值的频率,再找出最大值
2.3 LeetCode 236:二叉树的最近公共祖先
这道题要求找出二叉树中两个节点的最近公共祖先(LCA)。这是二叉树中的经典问题,有多种解法。
递归解法思路:
- 从根节点开始遍历
- 如果当前节点是p或q,则返回当前节点
- 递归查询左右子树
- 如果左右子树都返回非空结果,说明当前节点就是LCA
- 如果只有一边返回非空,则返回那边的结果
python复制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 if left else right
迭代解法思路:
- 使用父指针记录每个节点的父节点
- 从p节点回溯到根,记录路径
- 从q节点回溯,直到遇到第一个在p路径中的节点
3. 核心算法技巧总结
3.1 二叉搜索树的特性应用
二叉搜索树的中序遍历结果是一个有序数组,这一特性在解决530和501题时起到了关键作用。在实际应用中:
- 有序性利用:BST的中序遍历结果有序,可以避免排序操作
- 双指针技巧:在遍历过程中维护前驱指针,可以高效计算差值
- 频率统计:有序数组中的相同值必然连续,便于统计频率
3.2 二叉树遍历的递归与迭代
这三道题都涉及二叉树的遍历,递归和迭代是两种基本实现方式:
递归实现要点:
- 明确递归终止条件
- 设计合理的递归参数和返回值
- 注意递归栈的空间消耗
迭代实现要点:
- 使用栈模拟递归过程
- 注意入栈和出栈的顺序
- 可能需要额外的数据结构记录状态
3.3 最近公共祖先问题的变体
LCA问题有多种变体和应用场景:
- BST中的LCA:可以利用BST的特性简化查找
- 多叉树的LCA:原理类似,需要遍历所有子节点
- 带父指针的树:可以直接回溯构建路径
- 大量查询场景:可以使用Tarjan离线算法或二进制提升法优化
4. 常见问题与调试技巧
4.1 边界条件处理
在解决二叉树问题时,特别需要注意以下边界条件:
- 空树情况(root为None)
- 单节点树
- 所有节点值相同的树
- 退化成链表的树(极度不平衡)
- 节点就是自身的祖先的情况
4.2 递归调试技巧
递归代码容易写但难调试,以下技巧可以帮助排查问题:
- 添加详细的打印语句,显示递归深度和当前节点值
- 使用小规模的测试用例(3-5个节点)
- 绘制递归调用树,理清调用顺序
- 检查递归终止条件是否完备
- 验证返回值是否在所有路径都有返回
4.3 性能优化方向
对于树类问题,常见的性能优化手段包括:
- 剪枝:提前终止不必要的递归分支
- 记忆化:缓存重复计算的子树结果
- 迭代替代递归:减少函数调用开销
- Morris遍历:实现O(1)空间复杂度的遍历
- 并行计算:对左右子树可以并行处理(如果允许)
5. 扩展思考与实际应用
5.1 二叉搜索树在实际系统中的应用
BST及其变种(如AVL树、红黑树)在计算机系统中有广泛应用:
- 数据库索引(如B树、B+树)
- 内存中的有序数据结构(如Java的TreeMap)
- 文件系统的目录结构
- 网络路由表
- 事件调度器
5.2 最近公共祖先的应用场景
LCA算法不仅是一道面试题,在实际工程中也有重要应用:
- 计算Git提交历史的最近共同祖先
- 计算DOM树中两个元素的最近共同容器
- 计算家族谱系中的最近共同祖先
- 计算计算机网络中两个节点的最近交汇点
- 计算文件系统中两个路径的最近共同目录
5.3 算法思想的迁移应用
从这三道题中学到的算法思想可以迁移到其他问题:
- 双指针技巧:可用于链表、数组等问题
- 中序遍历思想:可用于任何需要有序处理的树结构
- 递归分解问题:可用于分治、动态规划等算法
- 回溯思想:可用于路径查找、状态空间搜索等问题
6. 训练建议与学习路径
6.1 针对二叉树的系统训练方法
要掌握二叉树相关算法,建议按照以下路径系统训练:
- 先掌握各种遍历方式(前序、中序、后序、层序)
- 理解递归和迭代的实现差异
- 练习基础题型(如求深度、节点数、对称性等)
- 进阶到经典问题(如LCA、序列化、重建等)
- 最后挑战综合应用题(如二叉搜索树迭代器、修剪BST等)
6.2 LeetCode刷题策略
针对LeetCode上的二叉树题目,建议采用以下策略:
- 按标签分类刷题(BST、普通二叉树、遍历、构造等)
- 每道题尝试多种解法(递归、迭代、Morris等)
- 总结同类题型的解题模板
- 记录易错点和边界条件
- 定期复习高频考题
6.3 面试准备要点
在面试中遇到二叉树问题时,注意以下要点:
- 先明确问题要求,确认输入输出
- 询问边界条件和特殊情况的处理
- 解释解题思路后再开始编码
- 编写清晰易读的代码,添加必要注释
- 设计测试用例验证代码正确性
- 分析时间空间复杂度
在实际编码中,我发现BST相关问题的关键在于充分利用有序性这一特性。对于530题,维护前驱节点的技巧可以扩展到其他需要比较相邻元素的场景。501题的众数查找方法也可以应用于流式数据中的频率统计。而236题的LCA解法展示了递归思想的强大之处——将复杂问题分解为简单的子问题。
最后分享一个实用技巧:在解决树问题时,先手动构建几个测试用例(包括普通情况和边界情况),在纸上模拟算法执行过程,这样能帮助快速发现思路中的漏洞。对于递归算法,特别要注意递归终止条件和返回值在所有路径上的完备性。
