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

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

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:"对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。"

文章目录

整体思路

这段代码旨在解决一个经典的树形问题:二叉树的最近公共祖先 (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。

完整代码

java 复制代码
// 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 是二叉树的高度。

参考灵神

相关推荐
阿群今天学习了吗27 分钟前
面向对象基础笔记
笔记·学习·算法
☞下凡☜2 小时前
C语言(20250717)
linux·c语言·算法
lemon_sjdk2 小时前
LWJGL教程(2)——游戏循环
java·人工智能·算法·游戏
weixin_524749962 小时前
MVCC(多版本并发控制)介绍及实现原理
java·数据库
盒子69102 小时前
leetcode丑数II计算第n个丑数
算法·leetcode·职场和发展
skylijf3 小时前
C++ Primer(第5版)- Chapter 7. Classes -004
java·开发语言
泉城老铁3 小时前
springboot+druid预防连接断开、断开后自动恢复
java·后端·架构
m1cr0wave3 小时前
javaweb的几大常见漏洞
java·安全·web
小蛋编程3 小时前
【图论】图的定义与一些常用术语
c++·算法
泉城老铁3 小时前
Spring Boot 中使用 Druid 连接池进行极致优化
java·后端·架构