图解路径总和(递归 + 非递归)

239 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情


大家好呀,我是帅蛋。

今天解决路径总和,如果你看过我【二叉树的所有路径】这篇文章,你会发现这其实是在它解法的基础上做了一些改动。

咱们话不多说,直接开整。

112-0

LeetCode 112:路径总和

题意

给定二叉树的根节点 root 和一个表示目标的整数 targetSum。

判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和 targetSum。存在返回 true,否则返回 false。

示例

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

输出:true

解释:等于目标和的根节点到叶节点路径如下图所示。

112-1

提示

  • 树中节点的数目在范围 [0, 5000] 内
  • -1000 <= Node.val <= 1000
  • -1000 <= targetSum <= 1000

题目解析

难度简单,如果你看过我【二叉树的所有路径】这篇文章,思路应该从你脑阔里立马跳出来。

如果你看过我足够多二叉树的文章,那你应该知道这个思路叫:自顶向下。

我在这里自顶向下用的其实就是前序遍历的方式,每次先判断当前的节点,再递归左子树,最后是右子树。

在本题路径总和中,自顶向下的解法其实就是:

  • 判断当前节点是否为叶子节点,如果是叶子节点,判断当前叶子节点的值是否 = targetSum 减去之前路径上节点值。
  • 递归左子树。
  • 递归右子树。

这种每次先根节点,再左子树,最后右子树,典型的前序遍历的方式。

112-2

递归法

递归法,那还是按照往常,祭出递归二步曲:

(1) 找出重复的子问题。

前序遍历的顺序是:根节点、左子树、右子树。

在本题同样也是这个顺序:判断节点,递归左子树,递归右子树

对于左子树和右子树来说,也都是同样的操作。

# 递归左子树
leftPath = self.hasPathSum(root.left, targetSum - root.val)
# 递归右子树
rightPath = self.hasPathSum(root.right, targetSum - root.val)

(2) 确定终止条件。

对于路径来说,遍历到叶子节点且叶子节点的值等于该路径之前节点的值,证明已找到,返回 True。

# 如果当前节点为叶子节点,且叶子节点的值等于减去该路径之前节点的值,返回 True
if root.left == None and root.right == None and root.val == targetSum:
    return True

最重要的两点确定完了,那总的代码也就出来了。

Python 代码实现

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
​
    def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
        # 如果树为空,返回 False
        if root == None:
            return False
        # 如果当前节点为叶子节点,且叶子节点的值等于减去该路径之前节点的值,返回 True
        if root.left == None and root.right == None and root.val == targetSum:
            return True
        # 递归左子树
        leftPath = self.hasPathSum(root.left, targetSum - root.val)
        # 递归右子树
        rightPath = self.hasPathSum(root.right, targetSum - root.val)
        
        return leftPath or rightPath

Java 代码实现

/**
 * Definition for a binary tree node.
 * public 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 boolean hasPathSum(TreeNode root, int targetSum) {
        // 如果树为空,返回 False
        if(root == null){
            return false;
        }
        // 如果当前节点为叶子节点,且叶子节点的值等于减去该路径之前节点的值,返回 True
        if(root.left == null && root.right == null && root.val == targetSum){
            return true;
        }
        // 递归左子树
        boolean leftPath = hasPathSum(root.left, targetSum - root.val);
        // 递归右子树
        boolean rightPath = hasPathSum(root.right, targetSum - root.val);
​
        return leftPath || rightPath;
    }
}

本题解,最坏情况下,每个节点都会被访问到,所以时间复杂度为 O(n)

此外在递归过程中调用了额外的栈空间,栈的大小取决于二叉树的高度,二叉树最坏情况下的高度为 n,所以空间复杂度为 O(n)

非递归法(迭代)

非递归的做法,很多人又叫迭代法迭代法就是不断地将旧的变量值,递推计算新的变量值

迭代法是用栈来做,对二叉树前序遍历迭代法不太了解的蛋粉可以看下面这篇文章:

ACM 选手带你玩转二叉树前中后序遍历(非递归版)

根据栈“先入后出”的特点,结合前序遍历的顺序,迭代的过程也就出来了:

每次都是先将节点放入栈,然后右子树,最后左子树。

具体步骤如下所示:

  • 初始化维护栈,将根节点及根节点的值入栈。
  • 当栈不为空时,弹出栈顶的节点及节点的值:
    • 若当前节点为叶子节点且累积值 val 为 targetSum,返回 true。
    • 若当前节点的右子树不为空,将右孩子及 “val + 右孩子的值”入栈。
    • 若当前节点的左子树不为空,将左孩子及”val + 左孩子的值“入栈。

以下图为例:

112-3

首先初始化栈 stack。

112-4

# 初始化栈
stack = [(root, root.val)]

当栈不为空,栈顶元素 node 和栈顶值 val 出栈。

112-5

node, val = stack.pop()

此时 node = 5,val = 5,此时 val 不等于 targetSum,此时 node 有左右子树,此时”右子树及路径值“先入栈,再”左子树及路径值“入栈。

112-6

# 右子树入栈
if node.right:
    stack.append((node.right, val + node.right.val))
# 左子树入栈
if node.left:
    stack.append((node.left, val + node.left.val))

接下来其实就是在重复上面的操作。

弹出栈顶元素 node = 4, val = 9,此时 val 不等于 targetSum,此时 node 有左子树,此时”左子树及路径值“入栈。

112-7

弹出栈顶元素 node =11, val = 20,此时 val 不等于 targetSum,此时 node 有左右子树,此时”右子树及路径值“先入栈,再”左子树及路径值“入栈。

112-8

弹出栈顶元素 node =7, val = 27,此时 node = 7 为叶子节点,但 val 不等于 targetSum。

112-9

继续弹出栈顶元素 node = 2,val = 22,此时 node 为叶子节点,且 val 等于 targetSum,证明该树中存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和 targetSum。

112-10

# 如果当前节点为叶子节点且路径总和 = targetSum
if node.left == None and node.right == None and val == targetSum:
    return True

Python 代码实现

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
        # 如果树为空,返回 False
        if root == None:
            return False
        # 初始化栈
        stack = [(root, root.val)]
​
        while stack:
            node, val = stack.pop()
            # 如果当前节点为叶子节点且路径总和 = targetSum
            if node.left == None and node.right == None and val == targetSum:
                return True
            # 右子树入栈
            if node.right:
                stack.append((node.right, val + node.right.val))
            # 左子树入栈
            if node.left:
                stack.append((node.left, val + node.left.val))
            
        return False

Java 代码实现

/**
 * Definition for a binary tree node.
 * public 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 boolean hasPathSum(TreeNode root, int targetSum) {
        // 如果树为空,返回 False
        if(root == null){
            return false;
        }
        // 初始化栈
        Stack<TreeNode> stack = new Stack<>();
        Stack<Integer> stackVal = new Stack<>();
        stack.push(root);
        stackVal.push(root.val);
​
        while(!stack.isEmpty()){
            int n = stack.size();
            for(int i = 0; i < n; i++){
                TreeNode node = stack.pop();
                int val = stackVal.pop();
                // 如果当前节点为叶子节点且路径总和 = targetSum
                if(node.left == null && node.right == null && val == targetSum){
                    return true;
                }
                // 右子树入栈
                if(node.right != null){
                    stack.push(node.right);
                    stackVal.push(val + node.right.val);
                }
                // 左子树入栈
                if(node.left != null){
                    stack.push(node.left);
                    stackVal.push(val + node.left.val);
                }
            }
        }
        return false;
    }
}

同样,本题解时间复杂度为 O(n)空间复杂度为 O(n)


图解路经总和到这就结束辣,你学废了嘛?

你看吧,其实数据结构与算法题啊,刷多了你就会发现似曾相识,切起题来就会一顺百顺。

这个道理我都交给你了,这不得马上安排个点赞嘛。

哈哈哈哈,我是帅蛋,我们下次见!