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

文章目录

前言

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

一、题目

1、原题链接

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

2、题目描述

二、个人思路整理

1、思路分析

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

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

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

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

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

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

  1. 结果汇总与判断:

    • left != nullptrright != nullptr:说明 pq 分别分布在当前节点的左子树和右子树中,因此当前节点 root 就是它们的最近公共祖先,直接返回 root

    • left != nullptrright == nullptr:说明目标节点都在左子树,返回 left。

    • left == nullptrright != nullptr:说明目标节点都在右子树,返回 right

    • 左右都为空:说明该子树不包含 pq,返回 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) 是本题的核心:最近公共祖先问题本质上依赖"自底向上"的搜索,即先处理左右子树,再根据左右子树的搜索结果判断当前节点是否为公共祖先。后序遍历的顺序是 左 -> 右 -> 根,这种"先子树、后根"的特性,天然契合本题需要先收集子树信息再汇总判断的需求。

算法核心思想

  • 自底向上搜索 :递归函数先深入左右子树查找 pq,再回到当前节点汇总结果,这正是后序遍历的天然顺序。
  • 左右子树汇总判断:若左右子树各找到一个目标节点,则当前节点即为最近公共祖先;若只有一侧找到,则向上传递找到的那一侧。
  • 终止条件即答案 :当节点为空或节点本身就是 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),但思路更直观、易于理解。

哈希表记录父节点思想

  • 核心思想 :遍历整棵树,建立 节点 -> 父节点 的映射表,从而获得从任意节点到根节点的"向上路径"。
  • 与本题的联系 :最近公共祖先本质上是 pq 两条向上路径的第一个交汇点。先标记 p 的所有祖先,再让 q 向上走,遇到的第一个已标记节点即为答案。
  • 注意事项 :该方法需要额外的 O ( n ) O(n) O(n) 空间存储父节点映射,且需要两次遍历(一次建表、一次查找),但胜在逻辑清晰、不易出错,适合作为递归解法的补充理解。

使用要点

  • 后序递归框架:先递归处理左右子树,再对左右结果做逻辑判断,这是本题递归解法的核心模板,务必牢记。
  • "遇到即返回"的剪枝 :当 rootpq 时直接返回,无需继续向下递归,因为若另一个目标在下方,当前节点就是它们的 LCA。
  • 空节点处理 :递归基中 root == nullptr 返回 nullptr,作为搜索失败的兜底,保证递归能正常回溯。
  • 结果传递:左右子树返回的非空结果向上层传递,最终在首个左右都非空的节点处汇聚成答案。

算法变体与扩展

  • 二叉搜索树的最近公共祖先(LeetCode 235) :利用 BST 的有序性,通过比较节点值大小即可在 O ( h ) O(h) O(h) 时间内定位 LCA,无需遍历整棵树,可与本题的通用解法对比理解。
  • 二叉树的层序遍历(LeetCode 102):与本题的深度优先搜索不同,层序遍历采用广度优先,可对比理解树的两种遍历方式。
  • 二叉树的最大深度(LeetCode 104):同样是后序遍历的经典应用,通过递归计算左右子树深度并取最大值,可加深对"自底向上"思想的理解。
  • 二叉树的所有路径(LeetCode 257):通过 DFS 记录从根到叶子的路径,与本题"向上路径"的思路互为镜像,可对比理解路径类问题的两种处理方向。

相关 LeetCode 例题

相关推荐
Doubbbbbbble云5 小时前
区间合并问题的常见算法模式与优化思路4
java·数据结构·算法
David猪大卫5 小时前
【C++修炼】哈希表的实现
数据结构·c++·笔记·学习·算法·哈希算法·散列表
linx2957 小时前
单元九 · 零基础路线图-第 7–9 周·类与 RAII
c语言·开发语言·数据结构·c++·嵌入式硬件·算法
布莱克6057 小时前
C++ 七大排序算法详解:选择、冒泡、插入、计数、堆、快速与归并排序
数据结构·c++·算法·排序算法
科学实验家9 小时前
最小生成树:Prim,kruskal
数据结构·c++·算法
小孩玩什么9 小时前
深入理解字符串匹配算法:BF算法,KMP算法
java·c语言·开发语言·数据结构·c++·算法
淡海水11 小时前
10-05-高级-模式匹配-CSharp如何改变与数据结构的交互方式
数据结构·算法·c#·模式匹配
蓝速科技11 小时前
蓝速科技 F100 双屏翻译机:中小企业跨国会议提效方案
大数据·网络·数据结构·人工智能·科技·运维开发
keyipatience12 小时前
5种IO模型与阻塞IO,select,poll,epoll,LT和ET模式
linux·服务器·网络·数据结构·c++·算法
彧azz21 小时前
图的存储结构详解:邻接矩阵的原理、实现与应用
开发语言·数据结构·学习·php