【二叉树】LC 236.二叉树的最近公共祖先

文章目录

前言

本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。

一、题目

1、原题链接

236.二叉树的最近公共祖先

2、题目描述

二、个人思路整理

1、思路分析

递归函数的作用可以定义为:在以 root 为根的子树中,寻找节点 p 或 q,并返回找到的节点或它们的最近公共祖先。

根据后序遍历"自底向上"的特性,每个节点会汇总左右子树的搜索结果:

  1. 递归终止条件(Base Case):

    • 如果当前节点 root 为空,说明没找到,返回 nullptr。
    • 如果当前节点 root 就是 p 或 q,直接返回 root(向上层汇报找到了目标)。
  2. 向左右子树递归(DFS):

  • 在左子树中查找:left = lowestCommonAncestor(root->left, p, q)

  • 在右子树中查找:right = lowestCommonAncestor(root->right, p, q)

  1. 结果汇总与判断:

    • 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 例题:

相关推荐
平行侠7 小时前
065 B树(B-Tree)
数据结构·b树·树查找
lvwangshu10 小时前
势能分析(摊还分析)
数据结构·数学·算法
All for pursuit.11 小时前
【贪心-4】581.最短无序连续子数组
数据结构·c++·算法·leetcode
mmmmath_313 小时前
LeetCode.020.有效的括号
数据结构·算法·leetcode
垆边人似月.13 小时前
华为机考题:字符个数统计
数据结构·算法·华为
All for pursuit.13 小时前
【回溯-2】22.括号生成
数据结构·c++·算法·leetcode
Chen—LSN14 小时前
数据结构——拿捏复杂度
java·c语言·开发语言·数据结构·经验分享·笔记·算法
Chen_harmony14 小时前
六、排序详解
c语言·数据结构·算法·排序算法
dtq042415 小时前
数据结构 - 树的概念
c语言·数据结构·学习·算法
H.莓飛15 小时前
【数据结构】栈
linux·开发语言·数据结构·算法·centos