Leetcode 236,437:二叉树的最近公共祖先,路径总和III

1.题目描述

题目解答

这道题让我们寻找二叉树的最近公共祖先,这道题上来要先理解题意,首先要是两个节点的祖先,其次要求是最近的,所以大概率不是根节点。

同时还有一个陷阱,那就是这两个节点也可能有一个为对方的祖先,他们两个节点不一定是再两个不同的子树中。

对于这道题我们可以使用后续遍历,后续遍历的特点是左右中,并且是由下自上的,我们可以利用这个特点,先分别看看左右子树是否存在目标值,如果在当前的节点发现左右子树都有要求的两个值,那么就确定一定是最近的公共祖先了。随后再将目标节点一路返回到最顶端。

java 复制代码
/**
 * 使用后序遍历来判断左右子树是否为空,从下向上返回值
 *
 * 整体框架(自底向上):
 *   1. 先递归左右子树,拿到它们各自的"搜索结果"
 *   2. 回到当前 root 时,根据左右结果组合出 4 种情况分别处理
 *
 * 递归返回值的 3 种含义(贯穿全文的关键):
 *   null   → 这棵子树里既没有 p 也没有 q
 *   p 或 q → 子树里找到了 p/q 其中的一个
 *   其他   → 子树内部已经确定的 LCA,往上继续传
 */
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        // base case(递归出口),一次判断三种情况:
        //   ① root==null:走到空节点,这条路没戏,返回 null
        //   ② root==p:当前节点就是 p,直接返回(不用再往 p 的子树里找 q,
        //      因为题目保证 p、q 都存在------若 q 在 p 子树里,LCA 恰好就是 p)
        //   ③ root==q:同上
        // 注意:== 比较的是"节点引用"(是不是同一个对象),不是比较 val
        if(root==null||root==q||root==p){
            return root;                    // 命中哪种就返回哪种(null / q / p)
        }

        // ===== 后序遍历位置:先递归子树,子树全部返回后才在下面处理 root =====
        // (这里先右后左;先左后右也一样,两棵子树互不干扰)

        // 去右子树里找:返回 null 表示右子树里没有 p 也没有 q
        TreeNode right = lowestCommonAncestor(root.right, p,  q);
        // 去左子树里找:含义同上
        TreeNode left = lowestCommonAncestor(root.left, p,  q);
        
        // 情况①:两边都非空 → p、q 分居 root 的两侧
        // root 是它们的"分叉点",就是最近公共祖先
        if(right!=null&&left!=null){
            return root;
        }

        // 情况②:只有右边找到 → p、q 都在右子树里,LCA 在右子树内部
        // right 可能是 p/q 本身,也可能是深层递归已经确定的 LCA,
        // 不用管是什么,原样向上传递即可(这也是为什么判断用 !=null 而不是 ==p)
        if(right!=null&&left==null){
            return right;
        }

        // 情况③:只有左边找到 → 和情况②完全对称
        if(right==null&&left!=null){
            return left;
        }

        // 情况④:两边都是 null → 整棵子树里没有 p 也没有 q
        // 向父节点报告"我这一片没有",让父节点去另一边找
        return null;
       
    }
}

2.题目描述

题目解答

1.递归查找

我们可以先定义一个方法,用来对于单个节点来寻找是否有题目中包含的路径,再对每个节点递归遍历,每次便利都调用这个方法,得出最终的值

java 复制代码
/**
 * 解法:双重递归(枚举所有起点 + 从起点向下走所有路径)
 *
 * 整体分工:
 *   外层 pathSum:负责"换起点"------让树里每个节点都有机会当一次路径起点
 *   内层 dfs:    负责"走路径"------从固定起点出发,向下延伸,
 *               每走到一个节点就检查一次 从起点到这里的和 是否等于目标
 *
 * 为什么不重不漏:
 *   路径的定义是"从某个祖先一路向下到某个后代",每条路径
 *   唯一对应一个(起点,终点)组合。外层保证每个节点都当过起点,
 *   内层保证每个起点走遍它下方所有节点 → 所有路径恰好被数一次
 *
 * 复杂度:O(n²)(每个起点最坏要走完它下方整棵子树)
 * 对比前缀和+哈希表解法:那是 O(n),但思路绕一些
 */
class Solution {

    int count = 0;    // 全局计数器:所有满足条件的路径总数
    public int pathSum(TreeNode root, int targetSum) {
        // base case:空树没有路径
        if(root==null){
            return 0;
        }

        // 第一件事:以当前 root 为起点,向下找出所有合法路径
        // temp 传 0:路径和从零开始,进去后第一步就加上 root 自己的值
        dfs(root,0,targetSum);

        // 第二件事:递归处理左右子树------换起点,让子树里的节点也当起点
        // 返回值故意不接:结果全累加在成员变量 count 里,用不上返回值
        pathSum(root.right,targetSum);
        pathSum(root.left,targetSum);

        // 走到这里时:以整棵树所有节点为起点的路径都数完了
        return count;
    }

    /**
     * 内层递归:从某个固定起点出发,向下延伸路径
     * temp:从起点一路加到当前节点的路径和
     */
    public void dfs(TreeNode root,long temp,int targetSum){
        if(root == null){
            return;
        }

        // 把当前节点纳入路径(起点到这里的和)
        temp+=root.val;

        // 每到一个节点就检查一次:起点→当前节点 的和是否等于目标
        // 等于 → 找到一条合法路径(终点就是当前节点),计数 +1
        // 注意:不是找到就停,路径还要继续往下延伸(后面可能有更大的负数绕回来)
        if(temp == targetSum){
            count++;
        }

        // 路径继续向下延伸到两个孩子(先右后左无所谓,互不干扰)
        // 路径只能"从上往下"走,所以只递归孩子节点,不会拐弯

        // temp 是方法参数(基本类型,值传递):每层递归拿到的是自己的副本,
        // 改动不影响上一层 → 天然自带"回溯"效果,返回上一层时 temp 自动还原
        // (对比:之前前缀和解法里的 map 是全局对象,才需要手动 -1 撤销)
        dfs(root.right,temp,targetSum);
        dfs(root.left,temp,targetSum);

    }
}

2.记录前缀和

我们还可以使用另一种方法,使用前缀和。建立一个哈希表和一个prefix变量,当每次递归遍历到子节点的时候,将子节点的值和prefix的值相加,以此类推,每次都可以获得当前节点从根节点开始一直累加的值,并将当前节点的值作为key,出现的次数为value存入哈希表。

当目前节点的值-目标值=之前在哈希表的key中出现的值的时候,就说明出现了一条路。

同时,这里还需要进行回溯,当目前节点已经到底的时候,我们需要将刚才存入的key进行归零,不然兄弟节点在遍历的时候可能会将对方子树中存储的key拿出来,导致额外的路径出现

java 复制代码
public void dfs(TreeNode root,int targetSum,Long prefix){
        if(root == null){
            return ;
        }
        prefix +=  root.val;
        count += map.getOrDefault(prefix - targetSum, 0);
        map.put(prefix,map.getOrDefault(prefix,0)+1);
        dfs(root.left,targetSum,prefix);
        dfs(root.right,targetSum,prefix);

        map.put(prefix,map.get(prefix)-1);


    }
相关推荐
数据狐(Datafox)1 小时前
1688商品详情API技术解析与落地应用(含标准 JSON 示例)
java·数据库·json
君顾11 小时前
AI智能商城实战指南:从架构设计到部署落地的完整技术方案
java·开发语言·多商户
Rain的Java大神之路1 小时前
SkyWalking从0-1部署成功实战
java·后端·架构
就叫飞六吧1 小时前
Spring 动态注册与移除 Bean 科普
java·后端·spring
IMPYLH1 小时前
HTML 的 <legend> 元素
java·前端·html
运行时异常1 小时前
WMS 仓储系统集成 AI Agent 实战】第 1 讲:从零搭建开发环境——pgvector 装不上、模型选错、依赖冲突,一天踩完三个大坑
java
夕除1 小时前
redis--008
java·jvm·数据库
wear工程师1 小时前
线程池队列满了会怎样?从拒绝策略看懂 ThreadPoolExecutor 的扩容顺序
java
晓晓_za8986681 小时前
多租户 GEO 优化系统源码架构:权限隔离与数据分离实现
java·tcp/ip·spring·缓存·微服务·架构