当前位置: 首页 > news >正文

Hot 100 --- 二叉树的最近公共祖先

本文概览:本文以LeetCode题目"二叉树的最近公共祖先"为例,讲解后序遍历+回溯汇总的思路,重点说明三种返回值情况的处理


一、题目

二、题目分析

题目要求:给定二叉树根节点root,以及两个节点pq,找到它们的最近公共祖先

最近公共祖先的定义:设节点root为节点pq的某公共祖先,若其左子节点root.left和右子节点root.right都不是pq的公共祖先,则称root是"最近的公共祖先"

根据这个定义,判断一个节点是不是最近公共祖先,就要看它的左右子节点是不是公共祖先——如果左右子节点都不是公共祖先,那当前节点就是最近公共祖先。最典型的情况就是pq分别位于当前节点的左右两侧

3 / \ 5 1 ← 3 是最近公共祖先(p=5 在左,q=1 在右) / \ \ 6 2 8

所以核心思路就是:后序遍历 + 回溯汇总。后序遍历先看左右子树,再把左右子树的结果汇总到当前节点做判断

思路概览

Java 实现代码如下

publicTreeNodelowestCommonAncestor(TreeNoderoot,TreeNodep,TreeNodeq){returndfs(root,p,q);}privateTreeNodedfs(TreeNodenode,TreeNodep,TreeNodeq){// 如果当前节点为空,返回nullif(node==null){returnnull;}// 如果当前节点是p或q,返回当前节点if(node==p||node==q){returnnode;}// 递归搜索左子树TreeNodeleft=dfs(node.left,p,q);// 递归搜索右子树TreeNoderight=dfs(node.right,p,q);// 如果左子树和右子树都返回了非null值,说明当前节点是最近公共祖先if(left!=null&&right!=null){returnnode;}// 如果左子树或右子树返回了非null值,说明最近公共祖先在该子树中returnleft!=null?left:right;}

思路简要说明

整体是后序遍历 + 回溯汇总:

  • 递归出口:当前节点为空返回 null;当前节点就是pq,直接返回自身
  • 后序遍历:先递归左子树、再递归右子树,拿到leftright两个返回值
  • 三种情况汇总
    • leftright都不为 null →pq分别在两侧,当前节点就是最近公共祖先
    • leftright只有一个不为 null → 把这个非 null 的值往上返回,让上层节点继续判断
    • leftright都为 null → 当前子树没找到,返回 null

核心就是每一步都把"子树里找到了什么"往上传,让上层节点做判断

三、思路详解

第一步:为什么是后序遍历?

要判断一个节点是不是最近公共祖先,必须先知道它的左子树和右子树里有没有pq。也就是说先处理左右子树,再处理当前节点——这正是后序遍历(左→右→根)的顺序

3 / \ 5 1 后序遍历顺序:5 → 1 → 3 遍历到 3 时,已经知道左子树找到了 5,右子树找到了 1 → 3 就是最近公共祖先

如果是前序遍历(根→左→右),到了 3 还没遍历左右子树,根本不知道下面有没有pq,没法判断

第二步:递归的两个出口

递归函数dfs(node, p, q)的作用是:在以node为根的子树中查找pq,返回找到的节点(或最近公共祖先)

出口 1:node == null

遍历到空节点,说明走到底了没找到,返回 null

出口 2:node == pnode == q

当前节点本身就是pq,直接返回自身。这里有一个关键点:一旦命中就直接返回,不再往下递归

为什么不往下递归?因为p(或q)已经找到了,它下面的子树再找也没意义。另一个节点只可能有两种位置:

  • 在它的子树里:那p(或q)自己就是最近公共祖先(祖先可以包含自己)
  • 不在它的子树里:那当前节点只是一个普通的目标节点,上层的其他分支会找到另一个,最后由上层汇总判断

不管哪种情况,当前节点只需要把自己返回给父节点就够了,不需要往下递归

第三步:左右子树返回值的三种情况

递归完左右子树后,拿到leftright两个返回值。这两个值有三种组合,每种对应一种情况:

情况 1:left != null && right != null(两边都不为空)

说明左子树找到了一个(pq),右子树也找到了另一个。此时当前节点就是最近公共祖先——pq分别在它的左右两侧

3 / \ 5 1 ← left=5, right=1,3 是最近公共祖先 / \ \ 6 2 8

返回当前节点node

情况 2:leftright只有一个不为 null

此时有两种子情况,但对代码来说处理方式完全一样:

  • 子情况 A:最近公共祖先就在这个非 null 的子树里,现在还没走到那一步,需要把这个非 null 的值继续往上传递,让上层节点去判断
  • 子情况 B:找到的就是p(或q)本身,另一个节点在它的子树下面,所以p(或q)自己就是最近公共祖先

不管是哪种子情况,处理方式都是:把非 null 的那个值往上返回

子情况A示例:最近祖先在子树深处,往上传递 3 / \ 5 null ← 5 子树里找到了 p、q,最近祖先是 5 / \ 6 2 ← 5 的 left=6 不为null,right=2 不为null → 5 是最近祖先 ← 3 的 left=5(返回的最近祖先),right=null → 把 5 往上传 子情况B示例:p 或 q 自己就是最近祖先 3 / \ 5 1 / \ 6 2 ← p=5, q=2,q 在 p 的子树里 ← 遍历到 5 时直接命中 p,返回 5 ← 3 的 left=5,right=null → 把 5 往上传,5 就是最近祖先

情况 3:leftright都为 null

说明左右子树都没找到pq,当前节点的子树里没有目标,返回 null

returnleft!=null?left:right;// 如果 left 不为 null 返回 left,否则返回 right// left 和 right 都为 null 时,返回 right(也是 null)// left 和 right 只有一个不为 null 时,返回那个非 null 的// left 和 right 都不为 null 时,上面已经 return 了,走不到这里

这一行代码同时处理了情况 2 和情况 3,很简洁

第四步:完整执行过程

以这棵树为例:

3 / \ 5 1 / \ \ 6 2 8

下面用三个例子分别演示三种情况。核心要盯住每个节点递归后拿到的leftright——左右子树返回了什么,决定了当前节点怎么处理


例1:p = 5,q = 1(p、q 分别在根的左右两侧)

初始:从根节点 3 开始

访问节点 3(当前路径:3)

  • 不是 p 也不是 q,递归左右子树

访问节点 5(当前路径:3→5)

  • 命中 p=5,直接返回 5,不再往下递归
  • → left = 5

访问节点 1(当前路径:3→1)

  • 命中 q=1,直接返回 1,不再往下递归
  • → right = 1

回到节点 3:left=5 不为 null,right=1 不为 null → 3 就是最近公共祖先,返回 3

结果:最近公共祖先是 3


例2:p = 5,q = 2(q 在 p 的子树里)

访问节点 3(当前路径:3)

  • 不是 p 也不是 q,递归左右子树

访问节点 5(当前路径:3→5)

  • 命中 p=5,直接返回 5,不再往下递归(2 虽然在 5 的子树里,但命中后不往下找)
  • → left = 5

访问节点 1(当前路径:3→1)

  • 不是 p 也不是 q,递归左右子树

访问节点 null(1 的左子树)

  • 空节点,返回 null
  • → left = null

访问节点 8(当前路径:3→1→8)

  • 不是 p 也不是 q,左右子树都是 null,返回 null
  • → right = null

回到节点 1:left=null,right=null → 返回 null

回到节点 3:left=5 不为 null,right=null → 把 5 往上传,返回 5

结果:最近公共祖先是 5(q=2 在 p=5 的子树里,p 自己就是最近祖先)


例3:p = 6,q = 2(都在左子树,最近祖先在深处)

访问节点 3(当前路径:3)

  • 不是 p 也不是 q,递归左右子树

访问节点 5(当前路径:3→5)

  • 不是 p 也不是 q,递归左右子树

访问节点 6(当前路径:3→5→6)

  • 命中 p=6,直接返回 6
  • → left = 6

访问节点 2(当前路径:3→5→2)

  • 命中 q=2,直接返回 2
  • → right = 2

回到节点 5:left=6 不为 null,right=2 不为 null → 5 就是最近公共祖先,返回 5

  • → 节点 3 的 left = 5

访问节点 1(当前路径:3→1)

  • 不是 p 也不是 q,递归左右子树
  • 左子树 null,右子树 8 也不是 p、q → left=null,right=null → 返回 null
  • → 节点 3 的 right = null

回到节点 3:left=5 不为 null,right=null → 把 5 往上传,返回 5

结果:最近公共祖先是 5(6 和 2 分别在 5 的左右两侧)


三个例子的共性

  • 例1:左右子树都返回非 null → 当前节点就是最近祖先
  • 例2、例3:只有一边返回非 null → 把这个非 null 的值往上传递,最终传到根节点的就是答案

不管最近祖先在哪个位置,它一定是"第一次出现 left 和 right 都不为 null"的那个节点,找到后就会一路被往上传

第五步:回溯汇总的本质

整个过程其实就是回溯汇总:每个节点把左右子树的查找结果汇总到一起,做一次判断,然后把结果往上传

  • 左右都找到了 → 当前节点就是最近祖先,把自己往上返回
  • 只有一边找到了 → 把那一边的结果往上返回,让上层继续判断
  • 两边都没找到 → 返回 null,告诉上层这里没有

最终结果会一层一层传递回根节点,根节点拿到的就是最终答案

这种"后序遍历先拿到子树结果,再在当前节点汇总"的模式,是二叉树问题中很常见的一种思路,适用于需要综合左右子树信息来做判断的场景

复杂度分析

  • 时间复杂度:O(n),每个节点最多遍历一次
  • 空间复杂度:O(h),递归栈深度等于树的高度,最坏情况 O(n)
http://www.jsqmd.com/news/1231348/

相关文章:

  • 2026年知识付费从业者选线上卖课加密工具哪家靠谱 - 热点品牌推荐
  • LAN Share Lite、Pro、Enterprise 怎么选
  • 2026镇江地区GEO关键词优化推广定制服务商推荐 - 奔跑123
  • 雕马设备齐全吗:雕马什么都有 - 17328623207
  • 公示|2026年7月劳力士香港官方售后服务中心网点地址与客服电话同步 - 劳力士服务中心
  • 大型 SaaS 产品的 Vite 迁移实录:从 Webpack 到 Vite 的 6 个月演进
  • 濮阳黄金回收避坑指南!6 家正规宝藏门店,全市区县全覆盖、绝不压价 - 资讯焦点
  • 2026 年更新:齐齐哈尔热门的透光泡沫铝板厂家哪家好,用它,让你的建筑光线翻倍的秘密 - 鉴选官
  • WAIC首个AI影视专场落幕,三个信号值得创作者关注
  • 2026美国本科申请,留学中介推荐看哪些核心指标? - 2027品牌AI展
  • MLOps核心原则:代码、模型、数据全方位版本管理
  • 2026年成都买新能源SUV选正规车行的实用指南 - 热点品牌推荐
  • 2026年门店管理系统实力团队哪家可靠选型参考指南 - 热点品牌推荐
  • 支付系统的分布式事务:两阶段提交与 TCC 的落地对比
  • 2026年玻璃钢透明屋面工厂怎么选择更靠谱省心 - 热点品牌推荐
  • 2026年福建水性环氧防静电自流平公司哪家强选择指南 - 热点品牌推荐
  • 2026年7月劳力士常州官方网点地址汇总,客户售后热线最新通知 - 劳力士官方服务中心
  • 期刊审稿意见要求降AI?4个免费方案最快当天搞定,不影响投稿周期
  • 2026年7月最新劳力士苏州吴江万象汇维修保养服务电话 - 劳力士官方服务中心
  • 手把手构建多智能体应用:基于LangGraph的投资组合分析系统
  • 2026年新发布:诚信PET采光瓦品牌厂商综合推荐与选型指南 - 品牌鉴赏官2026
  • 基于C++实现绘制已知函数的图像功能
  • 系统规划与管理师-人员培训与绩效管理体系建设
  • 电商场景下的AI智能客服:从意图识别到多轮对话的后端架构设计
  • 芝柏官方2026年7月最新公告:惠州客户服务网点地址与售后热线电话权威发布 - 亨得利钟表维修中心
  • 3个步骤将位图变矢量:SVGcode让像素图像无限缩放
  • 沈阳欧米茄2026年7月最新官方客户服务网点地址及热线信息公告 - 欧米茄官方服务中心
  • 宁夏银川周边小区园林景观设计服务机构怎么联系 - 热点品牌推荐
  • 职场文职增效方案|OpenClaw 本地自动化,5 分钟完成 Windows 11搭建
  • 平顶山黄金回收避坑指南!6 家正规宝藏门店,全市区县全覆盖、绝不压价 - 资讯焦点