【LeetCode Hot100 刷题日记 (49/100)】236. 二叉树的最近公共祖先 —— 后序遍历 + 递归回溯🌳

58 阅读5分钟

📌 题目链接:236. 二叉树的最近公共祖先 - 力扣(LeetCode)

🔍 难度:中等 | 🏷️ 标签:树、深度优先搜索(DFS)、递归、回溯

⏱️ 目标时间复杂度:O(n)

💾 空间复杂度:O(n)


题目分析

本题要求在普通二叉树(非二叉搜索树)中,找出两个给定节点 pq最近公共祖先(LCA, Lowest Common Ancestor)

关键点在于理解“最近公共祖先”的定义:

  • 节点 xpq 的祖先;
  • x 的深度尽可能大(即离 pq 最近);
  • 一个节点可以是它自己的祖先(如示例2所示)。

与二叉搜索树不同,普通二叉树没有大小顺序,无法通过值比较快速定位,因此必须遍历整棵树来判断位置关系。


核心算法及代码讲解

✅ 核心思想:后序遍历 + 递归回溯

要自底向上找 LCA,天然适合后序遍历(左 → 右 → 中) 。因为只有在左右子树都处理完之后,才能判断当前节点是否为 LCA。

🧠 递归函数的含义(非常重要!)

TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)

返回值含义:在以 root 为根的子树中,如果包含 pq,则返回“最近公共祖先的候选节点”;否则返回 nullptr

这个函数在最外层调用时返回的就是真正的 LCA;但在递归过程中,它可能返回的是 pq 本身,或某个中间候选节点。


📌 两种 LCA 情况(面试高频考点!)

  1. pq 分别位于当前节点的左右子树中
    → 当前节点就是 LCA。
  2. pq 的祖先(或反之)
    → 先遇到的那个节点(如 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;
    }
};

为什么能覆盖“自身是祖先”的情况?
假设 pq 的祖先。当递归到 p 时,root == p,直接返回 p。此时右子树(含 q)的递归结果会传上来,但左子树为 nullptr,最终 p 被一路返回至顶层,成为答案。


🧩解题思路(分步拆解)

  1. 明确遍历方式:必须后序遍历,因为需要左右子树的信息才能判断当前节点是否为 LCA。

  2. 设计递归终止条件

    • 遇到 nullptr → 返回 nullptr
    • 遇到 pq → 立即返回该节点(作为“找到”的信号)
  3. 递归获取左右子树结果

    • left:左子树中是否包含 pq 的“代表节点”
    • right:右子树同理
  4. 合并结果

    • leftright 都非空 → 当前 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!💪

📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!