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

【LeetCode 热题 100】236. 二叉树的最近公共祖先——DFS

Problem: 236. 二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(N)
    • 空间复杂度:O(H)

整体思路

这段代码旨在解决一个经典的树形问题:二叉树的最近公共祖先 (Lowest Common Ancestor of a Binary Tree)。问题要求在一棵普通的二叉树(非二叉搜索树)中,找到两个给定节点 pq 的最近公共祖先(LCA)。

该算法采用了一种非常优雅且高效的 深度优先搜索 (DFS) 的后序遍历 策略。其核心思想是自底向上地传递信息,通过递归函数的返回值来判断LCA的位置。

  1. 递归函数的定义与含义

    • lowestCommonAncestor(root, p, q) 这个递归函数的返回值具有特定的含义:
      • 如果在以 root 为根的子树中能找到 pq(或两者都能找到,但一个是另一个的祖先),函数就返回那个找到的节点 (pq)。
      • 如果在 root 的左右子树中分别找到了 pq,函数就返回 root 本身,因为 root 就是它们的LCA。
      • 如果在以 root 为根的子树中一个都找不到,函数就返回 null
  2. 递归的终止条件(Base Case)

    • if (root == null || root == p || root == q): 这是递归的出口。
      • root == null: 如果当前节点为空,说明这条路径走到底了,什么也没找到,返回 null
      • root == p || root == q: 如果当前节点就是 pq 中的一个,根据我们对函数返回值的定义,说明我们“找到”了目标,直接返回当前节点 root
  3. 递归的分解与合并(后序遍历逻辑)

    • 分解
      • TreeNode left = lowestCommonAncestor(root.left, p, q);:递归地在左子树中查找 pq
      • TreeNode right = lowestCommonAncestor(root.right, p, q);:递归地在右子树中查找 pq
    • 合并(这是后序遍历的核心,在处理完左右子树后进行判断):
      • if (left != null && right != null): 这是最关键的判断。如果 leftright 都不是 null,这意味着 pq 分别位于当前 root 节点的左右子树中。根据LCA的定义,当前 root 就是它们的最近公共祖先。因此,返回 root
      • return left != null ? left : right;: 如果上面那个 if 不成立,说明 pq 不在 root 的两侧。这种情况分为两种:
        • 只有一个子树找到了目标:例如,left 不是 nullrightnull。这说明 pq 都在左子树中,并且 left 返回的是它们中更“高”的那个(即它们的LCA)。我们只需将这个结果继续向上传递即可。所以返回 left。同理,如果只有右子树有结果,就返回 right
        • 两个子树都没找到leftright 都是 null,那么就返回 null
        • 这个三元表达式简洁地处理了这两种情况。

通过这种自底向上的信息传递,算法能够准确地在第一次相遇 pq 的分叉点处确定LCA。

完整代码

// Definition for a binary tree node.
// public class TreeNode {
//     int val;
//     TreeNode left;
//     TreeNode right;
//     TreeNode(int x) { val = x; }
// }class Solution {/*** 查找二叉树中两个给定节点 p 和 q 的最近公共祖先 (LCA)。* @param root 树的根节点* @param p 目标节点一* @param q 目标节点二* @return p 和 q 的最近公共祖先节点*/public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {// 步骤 1: 递归的终止条件 (Base Case)// 如果当前节点为空,或者当前节点就是 p 或 q,则直接返回当前节点。// - root == null: 搜索到了叶子节点的子节点,路径结束,返回 null。// - root == p || root == q: 找到了目标节点之一,返回该节点。if (root == null || root == p || root == q) {return root;}// 步骤 2: 递归分解 (后序遍历的前两步)// 在左子树中递归查找 p 或 q。TreeNode left = lowestCommonAncestor(root.left, p, q);// 在右子树中递归查找 p 或 q。TreeNode right = lowestCommonAncestor(root.right, p, q);// 步骤 3: 结果合并 (后序遍历的最后一步)// 基于左右子树的返回结果,判断当前 root 的角色。// Case 1: p 和 q 分别位于 root 的左右子树中。// 此时,left 返回 p (或其祖先),right 返回 q (或其祖先)。// 那么当前 root 就是它们的最近公共祖先。if (left != null && right != null) {return root;}// Case 2: p 和 q 都在同一侧子树中,或者其中一个是另一个的祖先。// - 如果 left 非空,right 为空,说明 p 和 q 都在左子树,left 就是它们在左子树中的LCA,继续向上传递。// - 如果 right 非空,left 为空,说明 p 和 q 都在右子树,right 就是它们在右子树中的LCA,继续向上传递。// - 如果都为空,说明此子树中没有 p 或 q,返回 null。// 这个三元表达式优雅地处理了以上所有情况。return left != null ? left : right;}
}

时空复杂度

时间复杂度:O(N)

  1. 遍历:该算法本质上是一次深度优先搜索(DFS)。在最坏的情况下,它需要访问树中的每一个节点一次。
  2. 节点操作:对于每个节点,执行的操作(比较、递归调用)都是常数时间的。

综合分析
算法的总时间复杂度与树中的节点数 N 成正比。因此,时间复杂度为 O(N)

空间复杂度:O(H)

  1. 主要存储开销:算法的空间开销主要来自于递归调用栈 (recursion call stack)
  2. 空间大小:递归栈的深度取决于树的高度 H
    • 最坏情况:如果树是一个极度不平衡的链状结构,树的高度 H 会约等于节点数 N。此时,空间复杂度为 O(N)
    • 最好情况(平衡二叉树):如果树是完全平衡的,树的高度 H 约等于 log N。此时,空间复杂度为 O(log N)

综合分析
算法所需的额外空间由递归栈的深度决定,因此空间复杂度为 O(H),其中 H 是二叉树的高度。

参考灵神

http://www.xdnf.cn/news/1156969.html

相关文章:

  • Effective Python 条款13:通过带星号的unpacking操作来捕获多个元素,不要用切片
  • 构建一个简单的Java框架来测量并发执行任务的时间
  • 深入浅出理解动态规划
  • The FastMCP Client
  • `tidyverse` 中涉及的函数及其用法
  • 【RAG Agent】Deep Searcher实现逻辑解析
  • 【JS逆向基础】数据库之redis
  • 第一章: 初识 Redis:背后的特性和典型应用场景
  • 什么是 ELK/Grafana
  • 使用pytorch创建模型时,nn.BatchNorm1d(128)的作用是什么?
  • Muduo库中单例模式详解
  • Mysql(事务)
  • 小型支付项目3-5:检测未接收到或未正确处理的支付回调通知
  • UE5多人MOBA+GAS 番外篇:移植Lyra的伤害特效(没用GameplayCue,因为我失败了┭┮﹏┭┮)
  • 音视频学习(四十一):H264帧内压缩技术
  • 【Vue进阶学习笔记】Vue 路由入门指南
  • 单线程 Reactor 模式
  • 动静态库的制作和原理
  • 【unitrix】 6.10 类型转换(from.rs)
  • [BUG]关于UE5.6编译时出现“Microsoft.MakeFile.Targets(44,5): Error MSB3073”问题的解决
  • 【软件测试】从软件测试到Bug评审:生命周期与管理技巧
  • VUE2 学习笔记2 数据绑定、数据代理、MVVM
  • 【数据结构】第一讲 —— 概论
  • 基于Arduino的智能寻迹小车设计
  • 剑指offer——链表:旋转数组的最小数字
  • 【OD机试】池化资源共享
  • 「Java案例」利用方法求反素数
  • Ubuntu挂载和取消挂载
  • LP-MSPM0G3507学习--07定时器之二定时节拍
  • ZYNQ平台深度剖析:EMMC/FLASH/SD卡性能测试与创新实践