Skip to content

Latest commit

 

History

History
872 lines (706 loc) · 27.3 KB

0112.路径总和.md

File metadata and controls

872 lines (706 loc) · 27.3 KB

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

递归函数什么时候需要返回值

相信很多同学都会疑惑,递归函数什么时候要有返回值,什么时候没有返回值,特别是有的时候递归函数返回类型为bool类型。

那么接下来我通过详细讲解如下两道题,来回答这个问题:

  • 112.路径总和
  • 113.路径总和ii

112. 路径总和

力扣题目链接

给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。

说明: 叶子节点是指没有子节点的节点。

示例:  给定如下二叉树,以及目标和 sum = 22,

112.路径总和1

返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。

思路

这道题我们要遍历从根节点到叶子节点的的路径看看总和是不是目标和。

递归

可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树

  1. 确定递归函数的参数和返回类型

参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。

再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:

  • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
  • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在236. 二叉树的最近公共祖先中介绍)
  • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)

而本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢?

如图所示:

112.路径总和

图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。

所以代码如下:

bool traversal(treenode* cur, int count)   // 注意函数的返回类型
  1. 确定终止条件

首先计数器如何统计这一条路径的和呢?

不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。

如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。

如果遍历到了叶子节点,count不为0,就是没找到。

递归终止条件代码如下:

if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0
if (!cur->left && !cur->right) return false; // 遇到叶子节点而没有找到合适的边,直接返回
  1. 确定单层递归的逻辑

因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。

递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。

代码如下:

if (cur->left) { // 左 (空节点不遍历)
    // 遇到叶子节点返回true,则直接返回true
    if (traversal(cur->left, count - cur->left->val)) return true; // 注意这里有回溯的逻辑
}
if (cur->right) { // 右 (空节点不遍历)
    // 遇到叶子节点返回true,则直接返回true
    if (traversal(cur->right, count - cur->right->val)) return true; // 注意这里有回溯的逻辑
}
return false;

以上代码中是包含着回溯的,没有回溯,如何后撤重新找另一条路径呢。

回溯隐藏在traversal(cur->left, count - cur->left->val)这里, 因为把count - cur->left->val 直接作为参数传进去,函数结束,count的数值没有改变。

为了把回溯的过程体现出来,可以改为如下代码:

if (cur->left) { //
    count -= cur->left->val; // 递归,处理节点;
    if (traversal(cur->left, count)) return true;
    count += cur->left->val; // 回溯,撤销处理结果
}
if (cur->right) { //
    count -= cur->right->val;
    if (traversal(cur->right, count)) return true;
    count += cur->right->val;
}
return false;

整体代码如下:

class solution {
private:
    bool traversal(treenode* cur, int count) {
        if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0
        if (!cur->left && !cur->right) return false; // 遇到叶子节点直接返回

        if (cur->left) { //
            count -= cur->left->val; // 递归,处理节点;
            if (traversal(cur->left, count)) return true;
            count += cur->left->val; // 回溯,撤销处理结果
        }
        if (cur->right) { //
            count -= cur->right->val; // 递归,处理节点;
            if (traversal(cur->right, count)) return true;
            count += cur->right->val; // 回溯,撤销处理结果
        }
        return false;
    }

public:
    bool haspathsum(treenode* root, int sum) {
        if (root == null) return false;
        return traversal(root, sum - root->val);
    }
};

以上代码精简之后如下:

class solution {
public:
    bool haspathsum(treenode* root, int sum) {
        if (root == null) return false;
        if (!root->left && !root->right && sum == root->val) {
            return true;
        }
        return haspathsum(root->left, sum - root->val) || haspathsum(root->right, sum - root->val);
    }
};

是不是发现精简之后的代码,已经完全看不出分析的过程了,所以我们要把题目分析清楚之后,在追求代码精简。 这一点我已经强调很多次了!

迭代

如果使用栈模拟递归的话,那么如果做回溯呢?

此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。

c++就我们用pair结构来存放这个栈里的元素。

定义为:pair<treenode*, int> pair<节点指针,路径数值>

这个为栈里的一个元素。

如下代码是使用栈模拟的前序遍历,如下:(详细注释)

class solution {

public:
    bool haspathsum(treenode* root, int sum) {
        if (root == null) return false;
        // 此时栈里要放的是pair<节点指针,路径数值>
        stack<pair<treenode*, int>> st;
        st.push(pair<treenode*, int>(root, root->val));
        while (!st.empty()) {
            pair<treenode*, int> node = st.top();
            st.pop();
            // 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true
            if (!node.first->left && !node.first->right && sum == node.second) return true;

            // 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来
            if (node.first->right) {
                st.push(pair<treenode*, int>(node.first->right, node.second + node.first->right->val));
            }

            // 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来
            if (node.first->left) {
                st.push(pair<treenode*, int>(node.first->left, node.second + node.first->left->val));
            }
        }
        return false;
    }
};

如果大家完全理解了本地的递归方法之后,就可以顺便把leetcode上113. 路径总和ii做了。

113. 路径总和ii

力扣题目链接

给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。

说明: 叶子节点是指没有子节点的节点。

示例: 给定如下二叉树,以及目标和 sum = 22,

113.路径总和ii1.png

思路

113.路径总和ii要遍历整个树,找到所有路径,所以递归函数不要返回值!

如图:

113.路径总和ii

为了尽可能的把细节体现出来,我写出如下代码(这份代码并不简洁,但是逻辑非常清晰

class solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    // 递归函数不需要返回值,因为我们要遍历整个树
    void traversal(treenode* cur, int count) {
        if (!cur->left && !cur->right && count == 0) { // 遇到了叶子节点且找到了和为sum的路径
            result.push_back(path);
            return;
        }

        if (!cur->left && !cur->right) return ; // 遇到叶子节点而没有找到合适的边,直接返回

        if (cur->left) { // 左 (空节点不遍历)
            path.push_back(cur->left->val);
            count -= cur->left->val;
            traversal(cur->left, count);    // 递归
            count += cur->left->val;        // 回溯
            path.pop_back();                // 回溯
        }
        if (cur->right) { // 右 (空节点不遍历)
            path.push_back(cur->right->val);
            count -= cur->right->val;
            traversal(cur->right, count);   // 递归
            count += cur->right->val;       // 回溯
            path.pop_back();                // 回溯
        }
        return ;
    }

public:
    vector<vector<int>> pathsum(treenode* root, int sum) {
        result.clear();
        path.clear();
        if (root == null) return result;
        path.push_back(root->val); // 把根节点放进路径
        traversal(root, sum - root->val);
        return result;
    }
};

至于113. 路径总和ii 的迭代法我并没有写,用迭代方式记录所有路径比较麻烦,也没有必要,如果大家感兴趣的话,可以再深入研究研究。

总结

本篇通过leetcode上112. 路径总和 和 113. 路径总和ii 详细的讲解了 递归函数什么时候需要返回值,什么不需要返回值。

这两道题目是掌握这一知识点非常好的题目,大家看完本篇文章再去做题,就会感受到搜索整棵树和搜索某一路径的差别。

对于112. 路径总和,我依然给出了递归法和迭代法,这种题目其实用迭代法会复杂一些,能掌握递归方式就够了!

其他语言版本

java

lc112

class solution {
   public boolean haspathsum(treenode root, int targetsum) {
        if (root == null) {
            return false;
        }
        targetsum -= root.val;
        // 叶子结点
        if (root.left == null && root.right == null) {
            return targetsum == 0;
        }
        if (root.left != null) {
            boolean left = haspathsum(root.left, targetsum);
            if (left) {// 已经找到
                return true;
            }
        }
        if (root.right != null) {
            boolean right = haspathsum(root.right, targetsum);
            if (right) {// 已经找到
                return true;
            }
        }
        return false;
    }
}

// lc112 简洁方法
class solution {
    public boolean haspathsum(treenode root, int targetsum) {
        
        if (root == null) return false; // 为空退出
        
        // 叶子节点判断是否符合
        if (root.left == null && root.right == null) return root.val == targetsum;

        // 求两侧分支的路径和
        return haspathsum(root.left, targetsum - root.val) || haspathsum(root.right, targetsum - root.val);
    }
}

迭代

class solution {
    public boolean haspathsum(treenode root, int targetsum) {
        if(root==null)return false;
        stack<treenode> stack1 = new stack<>();
        stack<integer> stack2 = new stack<>();
        stack1.push(root);stack2.push(root.val);
        while(!stack1.isempty()){
            int size = stack1.size();
            for(int i=0;i<size;i++){
                treenode node = stack1.pop();int sum=stack2.pop();
                // 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true
                if(node.left==null && node.right==null && sum==targetsum)return true;
                // 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来
                if(node.right!=null){
                    stack1.push(node.right);stack2.push(sum+node.right.val);
                }
                // 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来
                if(node.left!=null){
                    stack1.push(node.left);stack2.push(sum+node.left.val);
                }
            }
        }
        return false;
    }   
    
}

0113.路径总和-ii

class solution {
    public list<list<integer>> pathsum(treenode root, int targetsum) {
        list<list<integer>> res = new arraylist<>();
        if (root == null) return res; // 非空判断
        
        list<integer> path = new linkedlist<>();
        preorderdfs(root, targetsum, res, path);
        return res;
    }

    public void preorderdfs(treenode root, int targetsum, list<list<integer>> res, list<integer> path) {
        path.add(root.val);
        // 遇到了叶子节点
        if (root.left == null && root.right == null) {
            // 找到了和为 targetsum 的路径
            if (targetsum - root.val == 0) {
                res.add(new arraylist<>(path));
            }
            return; // 如果和不为 targetsum,返回
        }

        if (root.left != null) {
            preorderdfs(root.left, targetsum - root.val, res, path);
            path.remove(path.size() - 1); // 回溯
        }
        if (root.right != null) {
            preorderdfs(root.right, targetsum - root.val, res, path);
            path.remove(path.size() - 1); // 回溯
        }
    }
}
// 解法2
class Solution {
    List<List<Integer>> result;
    LinkedList<Integer> path;
    public List<List<Integer>> pathSum (TreeNode root,int targetSum) {
        result = new LinkedList<>();
        path = new LinkedList<>();
        travesal(root, targetSum);
        return result;
    }
    private void travesal(TreeNode root,  int count) {
        if (root == null) return;
        path.offer(root.val);
        count -= root.val;
        if (root.left == null && root.right == null && count == 0) {
            result.add(new LinkedList<>(path));
        }
        travesal(root.left, count);
        travesal(root.right, count);
        path.removeLast(); // 回溯
    }
}

python

0112.路径总和

递归

class solution:
    def haspathsum(self, root: treenode, targetsum: int) -> bool:
        def isornot(root, targetsum) -> bool:
            if (not root.left) and (not root.right) and targetsum == 0:
                return true  # 遇到叶子节点,并且计数为0
            if (not root.left) and (not root.right):
                return false  # 遇到叶子节点,计数不为0
            if root.left:
                targetsum -= root.left.val  # 左节点
                if isornot(root.left, targetsum): return true  # 递归,处理左节点
                targetsum += root.left.val  # 回溯
            if root.right:
                targetsum -= root.right.val  # 右节点
                if isornot(root.right, targetsum): return true  # 递归,处理右节点
                targetsum += root.right.val  # 回溯
            return false

        if root == none:
            return false  # 别忘记处理空treenode
        else:
            return isornot(root, targetsum - root.val)

迭代 - 层序遍历

class solution:
    def haspathsum(self, root: treenode, targetsum: int) -> bool:
        if not root: 
            return false

        stack = []  # [(当前节点,路径数值), ...]
        stack.append((root, root.val))

        while stack: 
            cur_node, path_sum = stack.pop()

            if not cur_node.left and not cur_node.right and path_sum == targetsum: 
                return true

            if cur_node.right: 
                stack.append((cur_node.right, path_sum + cur_node.right.val))    

            if cur_node.left: 
                stack.append((cur_node.left, path_sum + cur_node.left.val))

        return false

0113.路径总和-ii

递归

class solution:
    def pathsum(self, root: treenode, targetsum: int) -> list[list[int]]:

        def traversal(cur_node, remain): 
            if not cur_node.left and not cur_node.right and remain == 0: 
                result.append(path[:])
                return

            if not cur_node.left and not cur_node.right: return 

            if cur_node.left: 
                path.append(cur_node.left.val)
                remain -= cur_node.left.val
                traversal(cur_node.left, remain)
                path.pop()
                remain += cur_node.left.val

            if cur_node.right: 
                path.append(cur_node.right.val)
                remain -= cur_node.right.val
                traversal(cur_node.right, remain)
                path.pop()
                remain += cur_node.right.val

        result, path = [], []
        if not root: 
            return []
        path.append(root.val)
        traversal(root, targetsum - root.val)
        return result

go

  1. 路径总和
//递归法
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func hasPathSum(root *TreeNode, targetSum int) bool {
    if root == nil {
        return false
    }
    
    targetSum -= root.Val // 将targetSum在遍历每层的时候都减去本层节点的值
    if root.Left == nil && root.Right == nil && targetSum == 0 { // 如果剩余的targetSum为0, 则正好就是符合的结果
        return true
    }
    return hasPathSum(root.Left, targetSum) || hasPathSum(root.Right, targetSum) // 否则递归找
}
  1. 路径总和 II
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func pathSum(root *TreeNode, targetSum int) [][]int {
    result := make([][]int, 0)
    traverse(root, &result, new([]int), targetSum)
    return result
}

func traverse(node *TreeNode, result *[][]int, currPath *[]int, targetSum int) {
    if node == nil { // 这个判空也可以挪到递归遍历左右子树时去判断
        return
    }

    targetSum -= node.Val // 将targetSum在遍历每层的时候都减去本层节点的值
    *currPath = append(*currPath, node.Val) // 把当前节点放到路径记录里
    
    if node.Left == nil && node.Right == nil && targetSum == 0 { // 如果剩余的targetSum为0, 则正好就是符合的结果
        // 不能直接将currPath放到result里面, 因为currPath是共享的, 每次遍历子树时都会被修改
        pathCopy := make([]int, len(*currPath)) 
        for i, element := range *currPath {
            pathCopy[i] = element
        }
        *result = append(*result, pathCopy) // 将副本放到结果集里
    }

    traverse(node.Left, result, currPath, targetSum)
    traverse(node.Right, result, currPath, targetSum)
    *currPath = (*currPath)[:len(*currPath)-1] // 当前节点遍历完成, 从路径记录里删除掉
}

javascript

0112.路径总和

递归

/**
 * @param {treenode} root
 * @param {number} targetsum
 * @return {boolean}
 */
let haspathsum = function (root, targetsum) {
  // 递归法
  const traversal = (node, cnt) => {
    // 遇到叶子节点,并且计数为0
    if (cnt === 0 && !node.left && !node.right) return true;
    // 遇到叶子节点而没有找到合适的边(计数不为0),直接返回
    if (!node.left && !node.right) return false;

    //  左(空节点不遍历).遇到叶子节点返回true,则直接返回true
    if (node.left && traversal(node.left, cnt - node.left.val)) return true;
    //  右(空节点不遍历)  
    if (node.right && traversal(node.right, cnt - node.right.val)) return true;
    return false;
  };
  if (!root) return false;
  return traversal(root, targetsum - root.val);

  // 精简代码:
  // if (!root) return false;
  // if (!root.left && !root.right && targetsum === root.val) return true;
  // return haspathsum(root.left, targetsum - root.val) || haspathsum(root.right, targetsum - root.val);
};

迭代

let hasPathSum = function(root, targetSum) {
    if(root === null) return false;
    let nodeArr = [root];
    let valArr = [0];
    while(nodeArr.length) {
        let curNode = nodeArr.shift();
        let curVal = valArr.shift();
        curVal += curNode.val;
        // 为叶子结点,且和等于目标数,返回true
        if (curNode.left === null && curNode.right === null && curVal === targetSum) {
            return true;
        }
        // 左节点,将当前的数值也对应记录下来
        if (curNode.left) {
            nodeArr.push(curNode.left);
            valArr.push(curVal);
        }
        // 右节点,将当前的数值也对应记录下来
        if (curNode.right) {
            nodeArr.push(curNode.right);
            valArr.push(curVal);
        }
    }
    return false;
};

0113.路径总和-ii

递归

let pathsum = function (root, targetsum) {
  // 递归法
  // 要遍历整个树找到所有路径,所以递归函数不需要返回值, 与112不同
  const res = [];
  const travelsal = (node, cnt, path) => {
    // 遇到了叶子节点且找到了和为sum的路径
    if (cnt === 0 && !node.left && !node.right) {
      res.push([...path]); // 不能写res.push(path), 要深拷贝
      return;
    }
    if (!node.left && !node.right) return; // 遇到叶子节点而没有找到合适的边,直接返回
    // 左 (空节点不遍历)
    if (node.left) {
      path.push(node.left.val);
      travelsal(node.left, cnt - node.left.val, path); // 递归
      path.pop(); // 回溯
    }
    // 右 (空节点不遍历)
    if (node.right) {
      path.push(node.right.val);
      travelsal(node.right, cnt - node.right.val, path); // 递归
      path.pop(); // 回溯
    }
    return;
  };
  if (!root) return res;
  travelsal(root, targetsum - root.val, [root.val]); // 把根节点放进路径
  return res;
};

递归 精简版

var pathsum = function(root, targetsum) {
    //递归方法
    let respath = [],curpath = [];
    // 1. 确定递归函数参数
    const traveltree = function(node,count){
        curpath.push(node.val);
        count-=node.val;
        if(node.left===null&&node.right===null&&count===0){
            respath.push([...curpath]);
        }
        node.left&&traveltree(node.left,count);
        node.right&&traveltree(node.right,count);
        let cur = curpath.pop();
        count-=cur;
    }
    if(root===null){
        return respath;
    }
    travelTree(root,targetSum);
    return resPath;
};

迭代

let pathSum = function(root, targetSum) {
    if(root === null) return [];
    let nodeArr = [root];
    let resArr = []; // 记录符合目标和的返回路径
    let tempArr = [[]]; // 对应路径
    let countArr = [0]; //对应和
    while(nodeArr.length) {
        let curNode = nodeArr.shift();
        let curVal = countArr.shift();
        let curNodeArr = tempArr.shift();
        curVal += curNode.val;
        curNodeArr.push(curNode.val);
        // 为叶子结点,且和等于目标数,将此次结果数组push进返回数组中
        if (curNode.left === null && curNode.right === null && curVal === targetSum) {
            resArr.push(curNodeArr);
        }
        // 左节点,将当前的和及对应路径也对应记录下来
        if (curNode.left) {
            nodeArr.push(curNode.left);
            countArr.push(curVal);
            tempArr.push([...curNodeArr]);
        }
         // 右节点,将当前的和及对应路径也对应记录下来
        if (curNode.right) {
            nodeArr.push(curNode.right);
            countArr.push(curVal);
            tempArr.push([...curNodeArr]);
        }
    }
    return resArr;
};

Swift

0112.路径总和

递归

func hasPathSum(_ root: TreeNode?, _ targetSum: Int) -> Bool {
    guard let root = root else {
        return false
    }
    
    return traversal(root, targetSum - root.val)
}

func traversal(_ cur: TreeNode?, _ count: Int) -> Bool {
    if cur?.left == nil && cur?.right == nil && count == 0 {
        return true
    }
    
    if cur?.left == nil && cur?.right == nil {
        return false
    }
    
    if let leftNode = cur?.left {
        if traversal(leftNode, count - leftNode.val) {
            return true
        }
    }
    
    if let rightNode = cur?.right {
        if traversal(rightNode, count - rightNode.val) {
            return true
        }
    }
    
    return false
}

迭代

func hasPathSum(_ root: TreeNode?, _ targetSum: Int) -> Bool {
    guard let root = root else {
        return false
    }
    
    var stack = Array<(TreeNode, Int)>()
    stack.append((root, root.val))
    
    while !stack.isEmpty {
        let node = stack.removeLast()
        
        if node.0.left == nil && node.0.right == nil && targetSum == node.1 {
            return true
        }
        
        if let rightNode = node.0.right {
            stack.append((rightNode, node.1 + rightNode.val))
        }
        
        if let leftNode = node.0.left {
            stack.append((leftNode, node.1 + leftNode.val))
        }
    }
    
    return false
}

0113.路径总和 II

递归

var result = [[Int]]()
var path = [Int]()
func pathSum(_ root: TreeNode?, _ targetSum: Int) -> [[Int]] {
    result.removeAll()
    path.removeAll()
    guard let root = root else {
        return result
    }
    path.append(root.val)
    traversal(root, count: targetSum - root.val)
    return result

}

func traversal(_ cur: TreeNode?, count: Int) {
    var count = count
    // 遇到了叶子节点且找到了和为targetSum的路径
    if cur?.left == nil && cur?.right == nil && count == 0 {
        result.append(path)
        return
    }
    
    // 遇到叶子节点而没有找到合适的边,直接返回
    if cur?.left == nil && cur?.right == nil{
        return
    }
    
    if let leftNode = cur?.left {
        path.append(leftNode.val)
        count -= leftNode.val
        traversal(leftNode, count: count)// 递归
        count += leftNode.val// 回溯
        path.removeLast()// 回溯
    }
    
    if let rightNode = cur?.right {
        path.append(rightNode.val)
        count -= rightNode.val
        traversal(rightNode, count: count)// 递归
        count += rightNode.val// 回溯
        path.removeLast()// 回溯
    }
    return
}