算法题 路径总和

LeetCode 113. 路径总和 II

问题描述

给定二叉树的根节点 root 和一个整数 targetSum,找出所有从根节点到叶子节点的路径,其中路径上节点值的总和等于 targetSum

示例

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]
解释:
5 → 4 → 11 → 2 = 22
5 → 8 → 4 → 5 = 22

算法思路

深度优先搜索(DFS) + 回溯

  1. DFS遍历:从根节点开始递归遍历每个节点。
  2. 路径记录:维护当前路径列表,记录遍历过的节点值。
  3. 路径和更新:实时计算当前路径和(或传递累计和)。
  4. 叶子节点检查:到达叶子节点时,若路径和等于 targetSum,将当前路径加入结果。
  5. 回溯:递归返回前从路径中移除当前节点,确保路径状态正确。

代码实现

import java.util.ArrayList;
import java.util.List;

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        List<List<Integer>> result = new ArrayList<>();  // 存储所有符合条件的路径
        List<Integer> currentPath = new ArrayList<>();   // 存储当前遍历路径
        dfs(root, targetSum, 0, currentPath, result);    // 启动DFS
        return result;
    }

    /**
     * 深度优先搜索(DFS)辅助函数
     * 
     * @param node        当前遍历的节点
     * @param targetSum   目标和
     * @param currentSum  当前路径累计和
     * @param currentPath 当前路径(节点值列表)
     * @param result      结果集
     */
    private void dfs(TreeNode node, int targetSum, int currentSum, 
                    List<Integer> currentPath, List<List<Integer>> result) {
        if (node == null) return;  // 节点为空,直接返回
        
        currentPath.add(node.val);         // 将当前节点加入路径
        currentSum += node.val;            // 更新累计和
        
        // 叶子节点检查
        if (node.left == null && node.right == null) {
            if (currentSum == targetSum) {
                // 满足条件时,添加当前路径的拷贝到结果集
                result.add(new ArrayList<>(currentPath));
            }
        } else {
            // 递归遍历左右子树
            dfs(node.left, targetSum, currentSum, currentPath, result);
            dfs(node.right, targetSum, currentSum, currentPath, result);
        }
        
        // 回溯:移除当前节点,恢复路径状态
        currentPath.remove(currentPath.size() - 1);
        // 注意:currentSum 是局部变量,自动回溯,无需显式修改
    }
}

注释

  1. 初始化

    • result:存储所有符合条件的路径(列表的列表)。
    • currentPath:动态记录当前遍历路径的节点值。
  2. DFS 函数

    • 终止条件:节点为空时返回。
    • 记录路径
      • currentPath.add(node.val):将当前节点值加入路径。
      • currentSum += node.val:更新路径累计和。
    • 叶子节点处理
      • 左右子节点均为空时判定为叶子节点。
      • 若路径和等于 targetSum,将当前路径的拷贝加入 result(避免后续修改影响结果)。
    • 递归子树:非叶子节点时,继续遍历左右子树。
    • 回溯
      • currentPath.remove(size-1):移除当前节点,确保路径正确回溯。
      • currentSum 是局部变量(基本类型),递归返回时自动恢复,无需显式回退。

算法分析

  • 时间复杂度:O(n),每个节点访问一次。
  • 空间复杂度:O(n),递归栈深度和路径存储(最坏情况二叉树退化为链表)。
  • 核心点:DFS遍历 + 回溯维护路径状态。

算法过程

输入 root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum=22

  1. 遍历左子树
    • 路径:[5,4,11,7] → 和=27 ≠ 22 → 回溯移除 7
    • 路径:[5,4,11,2] → 和=22 → 加入结果。
  2. 遍历右子树
    • 路径:[5,8,13] → 和=26 ≠ 22 → 回溯。
    • 路径:[5,8,4,5] → 和=22 → 加入结果。
    • 路径:[5,8,4,1] → 和=18 ≠ 22 → 回溯。

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:示例
    TreeNode root = new TreeNode(5,
        new TreeNode(4,
            new TreeNode(11,
                new TreeNode(7),
                new TreeNode(2)),
        new TreeNode(8,
            new TreeNode(13),
            new TreeNode(4,
                new TreeNode(5),
                new TreeNode(1))));
    System.out.println("Test 1: " + solution.pathSum(root, 22)); 
    // 预期: [[5,4,11,2],[5,8,4,5]]
    
    // 测试用例2:空树
    System.out.println("Test 2: " + solution.pathSum(null, 0)); 
    // 预期: []
    
    // 测试用例3:单节点满足条件
    TreeNode root3 = new TreeNode(1);
    System.out.println("Test 3: " + solution.pathSum(root3, 1)); 
    // 预期: [[1]]
    
    // 测试用例4:无满足条件的路径
    TreeNode root4 = new TreeNode(1, new TreeNode(2), null);
    System.out.println("Test 4: " + solution.pathSum(root4, 3)); 
    // 预期: [](路径 1→2 和=3,但2不是叶子节点)
}

关键点

  1. 路径定义:必须从根节点到叶子节点(叶子节点无左右子树)。
  2. 回溯:确保递归返回时路径状态正确。
  3. 结果存储:添加路径时需创建新列表(new ArrayList<>(currentPath))。
  4. 局部变量currentSum 使用局部变量可避免显式回溯。
  5. 边界处理:空树、单节点树、无解情况需正确处理。

LeetCode 437. 路径总和 III

问题描述

给定二叉树的根节点 root 和一个整数 targetSum,找出路径和等于 targetSum 的路径数量。路径不需要从根节点开始,也不需要在叶子节点结束,但必须是从父节点指向子节点的向下路径

示例

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和为 8 的路径有:
1. 5 → 3
2. 5 → 2 → 1
3. -3 → 11

算法思路

前缀和 + 深度优先搜索(DFS)

  1. 前缀和
    • 序列的前n项和,对于一个数组a,其前缀和数组s定义为:
      • s[i] = a[0] + a[1] + … + a[i]
    • 例如,对于数组a = [1, 2, 3, 4, 5],其前缀和数组s为:
      • s = [1, 3, 6, 10, 15]
  2. 前缀和记录
    • 使用哈希表 prefixSumMap 记录从根节点到当前节点的路径上所有前缀和及其出现次数。
    • 初始化:prefixSumMap.put(0, 1)(空路径和为 0)。
  3. DFS遍历
    • 递归遍历每个节点,更新当前路径和 currentSum
    • 计算 currentSum - targetSum,若差值存在于哈希表中,则存在满足条件的路径。
    • 更新哈希表,递归左右子树,回溯时维护哈希表状态。

代码实现

import java.util.HashMap;
import java.util.Map;

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) {
        this.val = val;
        this.left = left;
        this.right = right;
    }
}

class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        // 哈希表记录前缀和及其出现次数
        Map<Long, Integer> prefixSumMap = new HashMap<>();
        prefixSumMap.put(0L, 1); // 空路径前缀和为0
        return dfs(root, targetSum, 0L, prefixSumMap);
    }

    /**
     * 深度优先搜索(DFS)辅助函数
     * 
     * @param node           当前节点
     * @param targetSum      目标和
     * @param currentSum     当前路径累计和
     * @param prefixSumMap   前缀和哈希表
     * @return               返回路径数量
     */
    private int dfs(TreeNode node, int targetSum, long currentSum, 
                   Map<Long, Integer> prefixSumMap) {
        if (node == null) return 0; // 节点为空,返回0
        
        currentSum += node.val; // 更新当前路径和
        int count = 0;
        
        // 检查是否存在路径满足 currentSum - prefixSum = targetSum
        count += prefixSumMap.getOrDefault(currentSum - targetSum, 0);
        
        // 更新当前路径和的出现次数
        prefixSumMap.put(currentSum, prefixSumMap.getOrDefault(currentSum, 0) + 1);
        
        // 递归遍历左右子树
        count += dfs(node.left, targetSum, currentSum, prefixSumMap);
        count += dfs(node.right, targetSum, currentSum, prefixSumMap);
        
        // 回溯:移除当前路径和,避免影响其他分支
        prefixSumMap.put(currentSum, prefixSumMap.get(currentSum) - 1);
        if (prefixSumMap.get(currentSum) == 0) {
            prefixSumMap.remove(currentSum);
        }
        
        return count;
    }
}

注释

  1. 初始化

    • prefixSumMap:存储前缀和及其出现次数,初始化 (0,1) 表示空路径和为 0。
  2. DFS 函数

    • 终止条件:节点为空时返回 0。
    • 更新路径和currentSum += node.val
    • 路径计数
      • currentSum - targetSum 的差值若在 prefixSumMap 中存在,则存在有效路径。
      • count 累加差值出现的次数。
    • 更新哈希表:当前路径和出现次数 +1。
    • 递归子树:遍历左右子树,累加路径数量。
    • 回溯维护
      • 将当前路径和的出现次数 -1。
      • 若次数为 0 则移除该键值(避免哈希表膨胀)。

算法分析

  • 时间复杂度:O(n),每个节点访问一次。
  • 空间复杂度:O(n),哈希表和递归栈空间(最坏情况树退化为链表)。
  • 核心点:前缀和思想 + 回溯维护哈希表状态。

算法过程

输入 root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum=8

  1. 遍历节点 10
    • currentSum=1010-8=2 → 无匹配。
    • 哈希表:{0:1, 10:1}
  2. 遍历节点 5
    • currentSum=1515-8=7 → 无匹配。
    • 哈希表:{0:1, 10:1, 15:1}
  3. 遍历节点 3
    • currentSum=1818-8=10 → 匹配到 10(路径:5→3)。
    • 哈希表:{0:1, 10:1, 15:1, 18:1}
  4. 最终匹配路径
    • 5 → 3(和=8)
    • 5 → 2 → 1(和=8)
    • -3 → 11(和=8)

测试用例

public static void main(String[] args) {
    Solution solution = new Solution();
    
    // 测试用例1:标准示例
    TreeNode root1 = new TreeNode(10,
        new TreeNode(5,
            new TreeNode(3,
                new TreeNode(3),
                new TreeNode(-2)),
            new TreeNode(2,
                null,
                new TreeNode(1))),
        new TreeNode(-3,
            null,
            new TreeNode(11)));
    System.out.println("Test 1: " + solution.pathSum(root1, 8)); // 3
    
    // 测试用例2:路径包含负值
    TreeNode root2 = new TreeNode(1,
        new TreeNode(-2,
            new TreeNode(1,
                new TreeNode(-1),
                null),
            new TreeNode(3)),
        new TreeNode(-3,
            new TreeNode(-2),
            null));
    System.out.println("Test 2: " + solution.pathSum(root2, -1)); // 4
    
    // 测试用例3:空树
    System.out.println("Test 3: " + solution.pathSum(null, 0)); // 0
    
    // 测试用例4:单节点满足条件
    TreeNode root4 = new TreeNode(5);
    System.out.println("Test 4: " + solution.pathSum(root4, 5)); // 1
    
    // 测试用例5:单节点不满足条件
    TreeNode root5 = new TreeNode(5);
    System.out.println("Test 5: " + solution.pathSum(root5, 3)); // 0
}

关键点

  1. 前缀和思想

    • 任意路径和 = currentSum - prefixSum
    • 查找 prefixSum = currentSum - targetSum
  2. 哈希表维护

    • 存储从根节点到当前节点的所有前缀和。
    • 回溯时及时移除当前路径和,避免污染其他分支。
  3. 路径定义

    • 路径必须向下(父节点→子节点)。
    • 起点不必是根节点,终点不必是叶子节点。
  4. 负值处理

    • 使用 long 类型避免整数溢出。
    • 哈希表需支持负值前缀和。
  5. 回溯必要性

    • 左右子树独立遍历,需维护哈希表状态一致性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值