📌 题目链接:236. 二叉树的最近公共祖先 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:树、深度优先搜索(DFS)、递归、回溯
⏱️ 目标时间复杂度:O(n)
💾 空间复杂度:O(n)
题目分析
本题要求在普通二叉树(非二叉搜索树)中,找出两个给定节点 p 和 q 的最近公共祖先(LCA, Lowest Common Ancestor) 。
关键点在于理解“最近公共祖先”的定义:
- 节点
x是p和q的祖先; x的深度尽可能大(即离p、q最近);- 一个节点可以是它自己的祖先(如示例2所示)。
与二叉搜索树不同,普通二叉树没有大小顺序,无法通过值比较快速定位,因此必须遍历整棵树来判断位置关系。
核心算法及代码讲解
✅ 核心思想:后序遍历 + 递归回溯
要自底向上找 LCA,天然适合后序遍历(左 → 右 → 中) 。因为只有在左右子树都处理完之后,才能判断当前节点是否为 LCA。
🧠 递归函数的含义(非常重要!)
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
返回值含义:在以
root为根的子树中,如果包含p或q,则返回“最近公共祖先的候选节点”;否则返回nullptr。
这个函数在最外层调用时返回的就是真正的 LCA;但在递归过程中,它可能返回的是 p、q 本身,或某个中间候选节点。
📌 两种 LCA 情况(面试高频考点!)
p和q分别位于当前节点的左右子树中
→ 当前节点就是 LCA。p是q的祖先(或反之)
→ 先遇到的那个节点(如p)就是 LCA。
💡 这两种情况在代码中被统一处理,无需显式区分!
🔍 代码逐行解析(C++)
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// 【终止条件】
// 如果当前节点为空,或等于 p 或 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 != nullptr && right != nullptr) {
// 情况1:p 和 q 分居左右 → root 就是 LCA
return root;
}
// 情况2:只有一边有结果 → 返回非空的那一边(可能是 p/q 本身,也可能是其祖先)
if (left == nullptr)
return right;
else
return left;
}
};
✅ 为什么能覆盖“自身是祖先”的情况?
假设p是q的祖先。当递归到p时,root == p,直接返回p。此时右子树(含q)的递归结果会传上来,但左子树为nullptr,最终p被一路返回至顶层,成为答案。
🧩解题思路(分步拆解)
-
明确遍历方式:必须后序遍历,因为需要左右子树的信息才能判断当前节点是否为 LCA。
-
设计递归终止条件:
- 遇到
nullptr→ 返回nullptr - 遇到
p或q→ 立即返回该节点(作为“找到”的信号)
- 遇到
-
递归获取左右子树结果:
left:左子树中是否包含p或q的“代表节点”right:右子树同理
-
合并结果:
- 若
left和right都非空 → 当前root是 LCA - 否则返回非空的一侧(向上传递“找到”的信息)
- 若
🎯 关键洞察:递归不是为了“找到 LCA”,而是为了传递“是否包含目标节点”的信息,并在合适时机“截获”LCA。
📊算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(n) —— 每个节点访问一次 |
| 空间复杂度 | O(n) —— 递归栈深度,最坏为链状树(高度 n) |
| 是否需要额外空间 | 否(纯递归,无哈希表等) |
| 适用场景 | 任意二叉树(不要求 BST 性质) |
| 面试加分点 | 能清晰解释“为什么后序遍历”、“如何统一处理两种 LCA 情况” |
💬 面试官可能会问:
- “如果树是 BST,能否优化?” → 可以利用 BST 性质,O(h) 时间解决。
- “能否用迭代实现?” → 理论可行(用栈模拟后序),但代码复杂,通常不推荐。
- “如果 p 或 q 不存在怎么办?” → 本题保证存在,但可扩展:需先验证存在性。
💻代码
✅C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
// Definition for a binary tree node.
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
// Base case:如果 root 是空,或就是 p 或 q,直接返回
if (!root || root == p || root == q) return root;
// 在左子树中找 p 或 q
TreeNode* left = lowestCommonAncestor(root->left, p, q);
// 在右子树中找 p 或 q
TreeNode* right = lowestCommonAncestor(root->right, p, q);
// 情况1:左右都找到 → 当前 root 就是最近公共祖先
if (left && right) return root;
// 情况2:只找到一个 → 把那个往上返回
return left ? left : right;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
// 构建示例1: [3,5,1,6,2,0,8,null,null,7,4], p=5, q=1
TreeNode* root = new TreeNode(3);
root->left = new TreeNode(5);
root->right = new TreeNode(1);
root->left->left = new TreeNode(6);
root->left->right = new TreeNode(2);
root->right->left = new TreeNode(0);
root->right->right = new TreeNode(8);
root->left->right->left = new TreeNode(7);
root->left->right->right = new TreeNode(4);
Solution sol;
TreeNode* p = root->left; // node 5
TreeNode* q = root->right; // node 1
TreeNode* ans = sol.lowestCommonAncestor(root, p, q);
cout << "LCA of 5 and 1: " << ans->val << "\n"; // 输出 3
p = root->left; // node 5
q = root->left->right->right; // node 4
ans = sol.lowestCommonAncestor(root, p, q);
cout << "LCA of 5 and 4: " << ans->val << "\n"; // 输出 5
return 0;
}
✅JavaScript
/**
* Definition for a binary tree node.
* function TreeNode(val) {
* this.val = val;
* this.left = this.right = null;
* }
*/
var lowestCommonAncestor = function(root, p, q) {
// Base case:如果 root 是空,或就是 p 或 q,直接返回
if (!root || root === p || root === q) return root;
// 在左子树中找 p 或 q
const left = lowestCommonAncestor(root.left, p, q);
// 在右子树中找 p 或 q
const right = lowestCommonAncestor(root.right, p, q);
// 情况1:左右都找到 → 当前 root 就是最近公共祖先
if (left && right) return root;
// 情况2:只找到一个 → 把那个往上返回
return left ? left : right;
};
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!