文章目录
前言
本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。
一、题目
1、原题链接
2、题目描述
二、个人思路整理
1、思路分析
递归函数的作用可以定义为:在以 root 为根的子树中,寻找节点 p 或 q,并返回找到的节点或它们的最近公共祖先。
根据后序遍历"自底向上"的特性,每个节点会汇总左右子树的搜索结果:
-
递归终止条件(Base Case):
- 如果当前节点
root为空,说明没找到,返回nullptr。 - 如果当前节点
root就是p或q,直接返回root(向上层汇报找到了目标)。
- 如果当前节点
-
向左右子树递归(DFS):
-
在左子树中查找:
left = lowestCommonAncestor(root->left, p, q) -
在右子树中查找:
right = lowestCommonAncestor(root->right, p, q)
-
结果汇总与判断:
-
left != nullptr且right != nullptr:说明p和q分别分布在当前节点的左子树和右子树中,因此当前节点root就是它们的最近公共祖先,直接返回root。 -
left != nullptr且right == nullptr:说明目标节点都在左子树,返回 left。 -
left == nullptr且right != nullptr:说明目标节点都在右子树,返回right。 -
左右都为空:说明该子树不包含
p或q,返回nullptr。
-
2、解题代码
cpp
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 1. 终止条件:到达空节点,或者找到了 p / q
if (root == nullptr || root == p || root == q) {
return root;
}
// 2. 分别在左右子树递归查找
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
// 3. 左右子树各找到一个,当前节点即为 LCA
if (left != nullptr && right != nullptr) {
return root;
}
// 4. 只有一侧找到了目标(或LCA),向上传递非空的那一侧
return left != nullptr ? left : right;
}
};
复杂度分析
- 时间复杂度: O ( N ) O(N) O(N),其中 N N N 是二叉树的节点数。最坏情况下需要遍历整棵树的所有节点。
- 空间复杂度: O ( N ) O(N) O(N),递归栈的深度取决于树的高度。最坏情况下(树退化为链表)为 O ( N ) O(N) O(N),平均情况下(平衡树)为 O ( log N ) O(\log N) O(logN)。
三、知识风暴
后序遍历(Postorder Traversal) 是本题的核心:最近公共祖先问题本质上依赖"自底向上"的搜索,即先处理左右子树,再根据左右子树的搜索结果判断当前节点是否为公共祖先。后序遍历的顺序是 左 -> 右 -> 根,这种"先子树、后根"的特性,天然契合本题需要先收集子树信息再汇总判断的需求。
算法核心思想:
- 自底向上搜索 :递归函数先深入左右子树查找
p和q,再回到当前节点汇总结果,这正是后序遍历的天然顺序。 - 左右子树汇总判断:若左右子树各找到一个目标节点,则当前节点即为最近公共祖先;若只有一侧找到,则向上传递找到的那一侧。
- 终止条件即答案 :当节点为空或节点本身就是
p/q时直接返回,这种"遇到即返回"的策略简化了边界处理。
常见对比:递归 vs 迭代(显式栈)
- 递归(后序 DFS) :这是本题最直观的解法,利用递归调用栈天然保存了回溯路径,代码简洁清晰;但空间复杂度为 O ( h ) O(h) O(h)( h h h 为树高),最坏情况下(退化为链表)递归深度为 O ( n ) O(n) O(n),存在栈溢出风险。
- 迭代(显式栈 + 标记) :用栈模拟后序遍历,需要额外标记每个节点的访问状态(如
pair<TreeNode*, bool>),以区分"首次入栈"和"左右子树处理完毕"两个阶段,实现相对繁琐但避免了系统递归栈的开销。 - 迭代(哈希表记录父节点) :这是另一种经典思路。先通过一次 BFS/DFS 遍历,用
unordered_map记录每个节点的父节点,然后从p出发向上标记祖先链,再从q出发向上查找第一个被标记的节点即为 LCA。空间复杂度为 O ( n ) O(n) O(n),但思路更直观、易于理解。
哈希表记录父节点思想:
- 核心思想 :遍历整棵树,建立
节点 -> 父节点的映射表,从而获得从任意节点到根节点的"向上路径"。 - 与本题的联系 :最近公共祖先本质上是
p和q两条向上路径的第一个交汇点。先标记p的所有祖先,再让q向上走,遇到的第一个已标记节点即为答案。 - 注意事项 :该方法需要额外的 O ( n ) O(n) O(n) 空间存储父节点映射,且需要两次遍历(一次建表、一次查找),但胜在逻辑清晰、不易出错,适合作为递归解法的补充理解。
使用要点:
- 后序递归框架:先递归处理左右子树,再对左右结果做逻辑判断,这是本题递归解法的核心模板,务必牢记。
- "遇到即返回"的剪枝 :当
root为p或q时直接返回,无需继续向下递归,因为若另一个目标在下方,当前节点就是它们的 LCA。 - 空节点处理 :递归基中
root == nullptr返回nullptr,作为搜索失败的兜底,保证递归能正常回溯。 - 结果传递:左右子树返回的非空结果向上层传递,最终在首个左右都非空的节点处汇聚成答案。
算法变体与扩展:
- 二叉搜索树的最近公共祖先(LeetCode 235) :利用 BST 的有序性,通过比较节点值大小即可在 O ( h ) O(h) O(h) 时间内定位 LCA,无需遍历整棵树,可与本题的通用解法对比理解。
- 二叉树的层序遍历(LeetCode 102):与本题的深度优先搜索不同,层序遍历采用广度优先,可对比理解树的两种遍历方式。
- 二叉树的最大深度(LeetCode 104):同样是后序遍历的经典应用,通过递归计算左右子树深度并取最大值,可加深对"自底向上"思想的理解。
- 二叉树的所有路径(LeetCode 257):通过 DFS 记录从根到叶子的路径,与本题"向上路径"的思路互为镜像,可对比理解路径类问题的两种处理方向。
相关 LeetCode 例题:
- 235. 二叉搜索树的最近公共祖先(利用 BST 有序性优化)
- 102. 二叉树的层序遍历(广度优先遍历对比)
- 104. 二叉树的最大深度(后序遍历求深度)
- 257. 二叉树的所有路径(DFS 路径记录对比)


