ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

二叉树最近公共祖先(LCA)的递归解法与实现

二叉树最近公共祖先(LCA)的递归解法与实现 1. 问题背景与定义最近公共祖先Lowest Common Ancestor简称LCA是树结构中的一个经典问题。在二叉树中给定两个节点p和q它们的最近公共祖先x需要满足以下条件x是p和q的祖先包括x就是p或q本身的情况在所有满足上述条件的节点中x的深度最大举个例子假设我们有一个家族树想找出两个人的最近共同祖先。这个共同祖先可能是他们的父母、祖父母甚至是他们自己中的一个如果一个人是另一个人的祖先。2. 递归解法核心思路2.1 递归的基本思想这个解法的精妙之处在于利用了二叉树的后序遍历特性左右根。我们从根节点开始先递归检查左子树再递归检查右子树最后处理当前节点。这种自底向上的遍历方式非常适合解决LCA问题。关键提示后序遍历的特点是先处理子节点再处理父节点这正好符合我们寻找最近祖先的需求。2.2 递归的终止条件递归需要明确的终止条件这里有三个当前节点为空NULL表示已经遍历到叶子节点的子节点当前节点就是p表示找到了p节点当前节点就是q表示找到了q节点遇到这三种情况时递归就会返回当前节点或NULL不再继续向下搜索。2.3 递归的四种情况分析在递归过程中对于每个节点我们需要考虑它的左右子树的返回结果这会产生四种可能左右子树都返回非NULL说明当前节点的左右子树分别包含p和q因此当前节点就是LCA只有左子树返回非NULL说明p和q都在左子树中返回左子树的结果只有右子树返回非NULL说明p和q都在右子树中返回右子树的结果左右子树都返回NULL说明当前子树中不包含p或q返回NULL3. 代码实现详解让我们仔细分析给出的C实现代码class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { // 终止条件遇到空节点或找到p/q if(!root || rootp || rootq) return root; // 递归搜索左子树 TreeNode* l lowestCommonAncestor(root-left, p, q); // 递归搜索右子树 TreeNode* r lowestCommonAncestor(root-right, p, q); // 情况1左右都非空当前节点是LCA if(l r) return root; // 情况2只有左非空返回左子树结果 if(l) return l; // 情况3返回右子树结果可能为空 return r; } };3.1 代码执行流程从根节点开始递归对于每个节点先检查是否满足终止条件递归处理左子树递归处理右子树根据左右子树的结果决定返回值递归最终会回溯到根节点返回最终的LCA3.2 时间复杂度分析每个节点最多被访问一次最坏情况下需要遍历整棵树时间复杂度O(n)其中n是树中节点数量3.3 空间复杂度分析递归调用栈的深度取决于树的高度平衡二叉树O(log n)最坏情况树退化为链表O(n)4. 示例解析让我们用题目中的示例2来具体走一遍算法流程输入root [3,5,1,6,2,0,8,null,null,7,4] p 5 q 4树结构3 / \ 5 1 / \ / \ 6 2 0 8 / \ 7 4执行步骤从节点3开始递归左子树节点5递归节点5的左子树节点6返回NULL递归节点5的右子树节点2递归节点2的左子树节点7返回NULL递归节点2的右子树节点4找到q返回节点4节点2左NULL右非NULL返回节点4节点5左NULL右返回节点4返回节点4递归右子树节点1递归处理最终返回NULL节点3左返回节点5右返回NULL返回节点5最终结果5与题目描述一致。5. 算法优化与变种5.1 非递归实现虽然递归实现简洁但我们可以用迭代方式父指针来避免递归栈的开销TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { unordered_mapTreeNode*, TreeNode* parent; stackTreeNode* stk; parent[root] nullptr; stk.push(root); // 记录p和q的父节点链 while (!parent.count(p) || !parent.count(q)) { TreeNode* node stk.top(); stk.pop(); if (node-left) { parent[node-left] node; stk.push(node-left); } if (node-right) { parent[node-right] node; stk.push(node-right); } } // 收集p的祖先链 setTreeNode* ancestors; while (p) { ancestors.insert(p); p parent[p]; } // 检查q的祖先链中第一个出现在p祖先链中的节点 while (!ancestors.count(q)) { q parent[q]; } return q; }这种方法的时间复杂度也是O(n)但空间复杂度在最坏情况下会达到O(n)。5.2 二叉搜索树的特殊情况如果题目中的二叉树是二叉搜索树(BST)我们可以利用BST的性质进行更高效的查找TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { if (p-val root-val q-val root-val) { return lowestCommonAncestor(root-left, p, q); } else if (p-val root-val q-val root-val) { return lowestCommonAncestor(root-right, p, q); } else { return root; } }BST版本的算法时间复杂度为O(h)其中h是树的高度空间复杂度为O(1)如果使用迭代实现。6. 常见错误与调试技巧6.1 常见实现错误混淆遍历顺序错误地使用前序或中序遍历导致无法正确判断祖先关系必须使用后序遍历因为我们需要先知道子节点的信息才能判断当前节点忽略节点自身是祖先的情况忘记处理p或q本身就是对方祖先的情况这就是为什么终止条件中要包含rootp || rootq错误处理返回值在判断左右子树结果时逻辑错误必须严格按四种情况处理返回值6.2 调试技巧可视化递归过程可以打印当前节点值和递归深度观察递归路径void helper(TreeNode* root, int depth) { cout string(depth*2, ) (root ? to_string(root-val) : null) endl; // ... }检查边界条件p或q是根节点p是q的祖先或反之树退化为链表的情况验证简单案例先手动验证小树2-3个节点再测试复杂情况7. 实际应用场景LCA算法在实际中有广泛的应用计算节点间距离两个节点之间的距离可以通过它们的LCA来计算distance depth(p) depth(q) - 2*depth(lca)版本控制系统Git等版本控制工具中合并分支时需要找到共同祖先提交家谱分析计算两个人的亲缘关系程度网络路由在计算机网络中寻找最优路由路径8. 扩展思考8.1 多叉树的LCA问题对于一般的树结构不一定是二叉树我们可以使用父指针法记录每个节点的父节点从两个节点向上回溯找到第一个公共祖先8.2 带频繁查询的LCA问题如果需要多次查询不同节点对的LCA可以考虑以下优化预处理使用Tarjan离线算法或倍增法预处理树结构建立索引为每个节点存储其所有祖先但这样空间开销较大8.3 非二叉树结构的LCA对于图结构中的LCA问题情况会更复杂可能有多个LCA需要先找到所有共同祖先然后选择深度最大的可以使用BFS或DFS结合标记法解决9. 个人实践心得在实际编码面试中二叉树LCA问题是一个高频题目。根据我的经验以下几点特别重要明确递归定义一定要清楚地定义递归函数的含义在这里是返回当前子树中p/q的LCA如果只存在一个则返回该节点画图辅助对于递归问题画出递归树和具体案例的执行流程非常有助于理解边界测试特别注意测试以下情况p或q是根节点p是q的父节点p和q在树的同一侧树退化为链表复杂度分析要能清晰解释时间复杂度和空间复杂度特别是递归栈的空间消耗变种准备掌握BST版本的LCA查找以及迭代实现方式这个算法最精妙的地方在于它如何通过简单的递归调用和返回值判断优雅地解决了看似复杂的问题。理解这一点后很多其他树相关问题如求节点距离、子树判断等都可以用类似的思路解决。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进