【忍者算法】从家族树到二叉树:深入解析最近公共祖先问题|LeetCode 236 二叉树的最近公共祖先
从生活场景理解LCA
想象你在整理家族族谱时,需要找到两位家族成员的最近共同祖先。比如小明和小红,他们的最近共同祖父可能不是年龄最大的祖先,而是离他们最近的共同长辈。在计算机科学中,二叉树中的最近公共祖先(LCA)问题与此如出一辙。
问题解析
LeetCode第236题要求:给定一个二叉树和两个节点p、q,找到这两个节点在树中的最近公共祖先。注意:
- 节点可以是自身的祖先
- p和q都存在于二叉树中
- 所有节点值唯一
递归法:优雅的分治策略
递归思路分解
我们可以把问题分解为三个层次来思考:
- 当前节点是否是p或q?
- 目标节点是否在左子树?
- 目标节点是否在右子树?
当出现以下情况之一时,当前节点就是LCA:
- p和q分别位于左右子树(即左右递归都返回非空)
- 当前节点是p/q,且另一个节点在其子树中
java
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 递归终止条件:找到目标节点或遍历到空节点
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
// 左右子树都找到目标节点,当前节点就是LCA
if (left != null && right != null) return root;
// 单边找到目标节点则向上传递
return left != null ? left : right;
}
}递归过程图解
以二叉树[3,5,1,6,2,0,8,null,null,7,4]为例,寻找节点5和1的LCA:
3
/ \
5 1 ← LCA是3
/ \ / \
6 2 0 8
/ \
7 4递归执行过程:
- 根节点3不是目标节点,递归左右子树
- 左子树5发现目标节点,右子树1发现另一个目标节点
- 根据判断条件,返回根节点3作为LCA
迭代法:显式记录访问路径
虽然递归法简洁优雅,但可能有读者更习惯迭代思维。我们可以通过记录访问路径的方式实现:
实现步骤
- 使用哈希表存储每个节点的父指针
- 记录从p到根节点的访问路径
- 从q出发向上查找,第一个出现在p路径中的节点即为LCA
java
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
// 建立父指针映射
Map<TreeNode, TreeNode> parent = new HashMap<>();
Deque<TreeNode> stack = new ArrayDeque<>();
parent.put(root, null);
stack.push(root);
// 构建完整父指针表
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (node.left != null) {
parent.put(node.left, node);
stack.push(node.left);
}
if (node.right != null) {
parent.put(node.right, node);
stack.push(node.right);
}
}
// 记录p的访问路径
Set<TreeNode> ancestors = new HashSet<>();
while (p != null) {
ancestors.add(p);
p = parent.get(p);
}
// 查找q路径中的第一个公共节点
while (!ancestors.contains(q)) {
q = parent.get(q);
}
return q;
}
}方法对比
| 方法 | 时间复杂度 | 空间复杂度 | 实现难度 |
|---|---|---|---|
| 递归法 | O(n) | O(h) | ★★☆☆☆ |
| 迭代法 | O(n) | O(n) | ★★★☆☆ |
递归法凭借其简洁性成为首选方案,迭代法则在特定场景下(如需要避免递归栈溢出)更具优势。
关键点解析
- 后序遍历特性:递归法本质上是后序遍历的应用,先处理子树再处理当前节点
- 短路优化:当找到目标节点时,可以提前终止不必要的搜索
- 边界处理:巧妙利用空值传递信息,空值表示子树中无目标节点
常见误区
- 误认为LCA必须是父节点(可能是祖父节点)
- 忘记处理节点自身是祖先的情况
- 混淆节点值和节点引用(Java中要比较对象地址)
实际应用
- Git版本控制中的最近共同提交
- 社交网络中的共同联系人查找
- 生物信息学中的进化树分析
- 编译器中的符号解析
进阶思考
- 如果树节点包含父指针,如何优化算法?
- 如何处理多个目标节点的LCA问题?
- 当树结构动态变化时,如何高效维护LCA信息?
- 在N叉树中如何寻找LCA?
总结
LCA问题教会我们:
- 递归思维在树结构问题中的强大威力
- 通过分治策略将复杂问题拆解为子问题
- 不同数据结构组合带来的性能优化空间
- 算法设计中对边界条件的敏感性
无论是递归法还是迭代法,其核心都在于理解树结构的遍历特性。就像在家族中寻找共同祖先一样,算法本质上是在建立节点间的联系网络。
欢迎关注我的公众号【忍者算法】,回复【二叉树】获取精心整理的二叉树专题刷题手册。包含:
- 高频面试题分类解析
- 递归与非递归模板代码
- 经典题目变形题解
- 大厂真题实战演练
让我们一起在算法的世界里修炼忍者之道!🗡️