二叉树最近公共祖先(LCA)问题详解与实现

发布时间:2026/7/30 21:52:41
二叉树最近公共祖先(LCA)问题详解与实现 1. 问题背景与定义理解最近公共祖先Lowest Common Ancestor简称LCA是二叉树算法中的经典问题。以LeetCode 236题为例给定一个二叉树和两个节点p、q要求找到这两个节点在树中最低的公共祖先节点。这里的最低指的是离根节点最远的那个公共祖先。举个例子假设我们有以下二叉树3 / \ 5 1 / \ / \ 6 2 0 8 / \ 7 4如果p5q1那么LCA就是3如果p5q4那么LCA就是5本身如果p7q8那么LCA就是3这个问题在实际开发中有广泛应用场景比如Git版本控制中寻找两个分支的最近共同提交DOM树中寻找两个元素的最近共同父元素家谱系统中寻找两个人的最近共同祖先2. 递归解法详解2.1 递归思路分析递归解法的核心思想是后序遍历左右根顺序因为我们需要先知道左右子树的情况才能处理当前节点。基本逻辑如下如果当前节点是null返回null如果当前节点就是p或q直接返回当前节点递归处理左子树和右子树如果左右子树都返回非null说明当前节点就是LCA如果只有左子树返回非null返回左子树的结果如果只有右子树返回非null返回右子树的结果2.2 递归代码实现class TreeNode: def __init__(self, x): self.val x self.left None self.right None 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: # 左右都找到当前节点就是LCA return root return left if left else right # 返回非空的那个2.3 递归解法的时间复杂度递归解法的时间复杂度是O(n)其中n是树中的节点数因为每个节点最多被访问一次。空间复杂度在最坏情况下树退化为链表是O(n)平均情况下是O(h)h是树的高度。提示递归解法虽然简洁但在处理大型树时可能会遇到栈溢出问题。对于特别深的树迭代解法可能更安全。3. 迭代解法详解3.1 迭代思路分析迭代解法的核心是使用哈希表记录每个节点的父节点然后通过回溯p和q的祖先链来找到它们的最近公共祖先。具体步骤使用栈进行迭代遍历整棵树记录每个节点的父节点从p节点开始回溯到根节点记录所有访问过的祖先节点从q节点开始回溯第一个在p的祖先集合中出现的节点就是LCA3.2 迭代代码实现class Solution: def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) - TreeNode: # 使用栈进行迭代遍历 stack [root] parent {root: None} # 迭代直到找到p和q的父节点关系 while p not in parent or q not in parent: node stack.pop() if node.left: parent[node.left] node stack.append(node.left) if node.right: parent[node.right] node stack.append(node.right) # 收集p的所有祖先 ancestors set() while p: ancestors.add(p) p parent[p] # 在q的祖先链中寻找第一个公共祖先 while q not in ancestors: q parent[q] return q3.3 迭代解法的性能分析迭代解法的时间复杂度同样是O(n)因为每个节点最多被访问两次一次在遍历时一次在回溯时。空间复杂度是O(n)因为需要存储所有节点的父节点关系。注意迭代解法虽然代码稍长但避免了递归的栈溢出风险在处理深度很大的树时更可靠。4. 两种解法的对比与选择4.1 时间复杂度对比两种解法在最坏情况下都是O(n)时间复杂度但实际运行时间可能有差异递归解法通常更快因为函数调用开销较小迭代解法需要额外的哈希表存储父节点关系内存占用稍高4.2 适用场景选择选择递归解法的情况树的高度不会太大避免栈溢出代码简洁性更重要面试中通常更倾向于递归解法选择迭代解法的情况树可能非常深防止栈溢出需要更可控的内存使用可能需要扩展功能如多次查询LCA4.3 实际测试数据在LeetCode测试用例中递归解法平均运行时间80ms迭代解法平均运行时间100ms内存使用递归解法通常少用10-20%5. 常见错误与调试技巧5.1 递归解法常见错误忘记处理基准情况# 错误示例 if not root: return None # 漏掉了 root p or root q 的情况错误理解返回值# 错误示例 if left and right: return root elif left: # 这里不应该有elif会导致漏掉某些情况 return left else: return right混淆节点值和节点对象# 错误示例 if root.val p.val or root.val q.val: # 应该直接比较节点对象 return root5.2 迭代解法常见错误父节点记录不完整# 错误示例 while stack and (p not in parent or q not in parent): # 可能提前退出循环回溯时无限循环# 错误示例 while q: # 应该检查q是否在ancestors中 if q in ancestors: return q q parent[q]初始条件处理不当# 错误示例 if not root: # 应该先检查p或q是否是root return None5.3 调试技巧可视化小树手工绘制简单的二叉树逐步跟踪算法执行过程打印关键变量在递归解法中打印当前节点和左右子树返回值边界测试测试p或q是根节点、p是q的祖先等情况使用LeetCode可视化工具观察实际执行过程6. 算法优化与变种问题6.1 多次查询优化如果需要多次查询不同节点对的LCA可以使用Tarjan离线算法或二进制提升技术进行预处理将每次查询的时间复杂度降到O(1)。6.2 二叉搜索树的LCA对于二叉搜索树(BST)可以利用BST的性质简化算法def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) - TreeNode: while root: if p.val root.val and q.val root.val: root root.left elif p.val root.val and q.val root.val: root root.right else: return root6.3 带父指针的树如果树节点包含指向父节点的指针问题可以简化为两个链表的交点问题分别获取p和q到根节点的路径长度将较长的路径先前进差值步然后同时前进直到找到相同节点6.4 N叉树的LCA对于N叉树递归解法可以扩展为def lowestCommonAncestor(self, root: Node, p: Node, q: Node) - Node: if not root or root p or root q: return root count 0 res None for child in root.children: curr self.lowestCommonAncestor(child, p, q) if curr: count 1 res curr if count 2: return root return res7. 实际工程应用案例7.1 Git版本控制Git使用类似LCA的算法来寻找两个分支的合并基础。当执行git merge时系统会寻找两个提交的最近共同祖先作为三方合并的基础。7.2 DOM树操作在Web开发中需要确定两个DOM元素的最近共同祖先来实现事件委托或样式继承。现代浏览器原生提供了Node.compareDocumentPosition()方法但理解其底层原理很重要。7.3 文件系统路径在文件系统中寻找两个文件或目录的最低共同父目录也属于LCA问题。例如/home/user/projects/app/src/main.js /home/user/projects/docs/README.md最低共同父目录是/home/user/projects7.4 网络路由在网络路由中寻找两个IP地址的最长公共前缀可以建模为LCA问题用于优化路由表查找。8. 面试准备建议8.1 常见面试问题如何证明你的算法是正确的如果树很大递归解法会有什么问题如何修改算法处理节点不在树中的情况如果允许节点引用父节点如何优化算法如何扩展算法处理N叉树8.2 白板编程技巧先明确问题定义和边界条件画出一个具体的二叉树例子逐步解释递归或迭代的过程讨论时间复杂度和空间复杂度考虑可能的优化和变种8.3 代码风格建议为TreeNode类添加清晰的注释使用有意义的变量名如ancestors而不是s添加必要的空值检查保持一致的代码缩进和格式为复杂逻辑添加注释9. 扩展学习资源9.1 推荐练习题LeetCode 235. 二叉搜索树的最近公共祖先LeetCode 1644. 二叉树的最近公共祖先 II节点可能不存在LeetCode 1650. 二叉树的最近公共祖先 III带父指针LeetCode 1676. 二叉树的最近公共祖先 IV多个节点9.2 进阶算法学习Tarjan离线LCA算法二进制提升技术欧拉序与RMQ并查集在LCA问题中的应用9.3 参考书籍《算法导论》- 第21章 数据结构和不相交集合《编程珠玑》- 算法设计技术《剑指Offer》- 树相关面试题《算法竞赛入门经典》- 树结构高级应用