最近公共祖先(Lowest Common Ancestor, LCA)是二叉树算法中的经典问题,在技术面试和算法竞赛中频繁出现。我第一次遇到这个问题是在准备某大厂面试时,当时被要求在白板上手写解法,结果因为对递归理解不够深入而惨遭淘汰。经过反复练习和总结,我发现这个问题其实蕴含着二叉树遍历的精髓。
LCA问题的标准定义是:对于有根树T的两个节点p和q,最近公共祖先表示为一个节点x,满足x是p、q的祖先且x的深度尽可能大。这里需要注意"祖先"的定义包含节点自身——也就是说,如果p是q的父节点,那么p就是它们的LCA。
关键理解:LCA不一定是p和q的直接父节点,它可能在树的更高层。这是很多初学者容易混淆的地方。
我最初想到的解法是两遍遍历策略,这种方法的优势在于思路直观,容易理解和实现。基本思路分为两个阶段:
这种方法虽然需要遍历两次树,但时间复杂度仍然是O(n),在大多数情况下性能足够好。
统计阶段的DFS实现有几个关键点需要注意:
cpp复制int dfs(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr) return 0;
int count = 0;
if(root == p || root == q) count += 1;
count += dfs(root->left, p, q) + dfs(root->right, p, q);
hasCount[root] = count;
return count;
}
实现要点:
hasCount记录每个节点的统计结果踩坑记录:我曾忘记在递归前判断root是否为nullptr,导致段错误。这是二叉树递归中的常见错误,务必注意边界条件。
查找阶段的helper函数需要处理三种典型情况:
cpp复制TreeNode* helper(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == nullptr) return root;
// 情况一:当前节点是目标节点,且子树包含另一个
if(root == p || root == q) {
if(hasCount[root->left]==1 || hasCount[root->right]==1) {
return root;
}
}
// 情况二:左右子树各包含一个目标节点
if(hasCount[root->left] == 1 && hasCount[root->right] == 1) {
return root;
}
// 情况三:递归查找子树
TreeNode* res = helper(root->left, p, q);
if(res != nullptr) return res;
return helper(root->right, p, q);
}
情况分析:
两遍遍历解法的复杂度分析如下:
| 指标 | 统计阶段 | 查找阶段 | 总计 |
|---|---|---|---|
| 时间复杂度 | O(n) | O(n) | O(n) |
| 空间复杂度 | O(n) | O(h) | O(n) |
其中:
哈希表hasCount需要O(n)的额外空间,这是空间复杂度的主要瓶颈。
经过多次练习后,我发现其实可以只用一遍遍历就解决问题:
cpp复制TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == nullptr || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left && right) return root;
return left ? left : right;
}
优化点分析:
这个优化版本是我现在面试时的首选方案,它体现了对递归和二叉树遍历的深刻理解。
在解决LCA问题时,我遇到过以下几种常见错误:
边界条件处理不当:
递归逻辑错误:
特殊场景考虑不周:
为了验证算法的正确性,我总结了一套测试方法:
必备测试用例:
常规二叉树,p和q在不同子树
code复制 3
/ \
5 1
/ \ / \
6 2 0 8
/ \
7 4
p是q的祖先
code复制 3
/ \
5 1
/
6
p或q是根节点
树退化为链表
code复制 1
/
2
/
3
调试技巧:
掌握了基础LCA解法后,可以解决一系列变种问题:
二叉搜索树的LCA(LeetCode 235)
带父指针的树(LintCode 474)
多节点LCA(LCA for K nodes)
任意树的LCA(Tarjan离线算法)
LCA算法在实际开发中有多种应用:
从最初被这个问题难倒,到现在能熟练写出多种解法,我总结了以下几点经验:
在实际面试中,我建议先解释两遍遍历的思路,展示对问题的理解,然后再提出优化方案。这样既能体现扎实的基础,又能展示优化能力。