二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?

Posted 代码随想录

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?相关的知识,希望对你有一定的参考价值。

给「代码随想录」一个星标吧!

就看是否需要遍历整个二叉树

112. 路径总和

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

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

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

二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?

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

思路

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

递归

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

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

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

「再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?」

在文章中,我给出了一个结论:

「如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。」

在中,因为要遍历树的所有路径,找出深度最深的叶子节点,所以递归函数不要返回值。

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

如图所示:

二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?

图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用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 - cur->right->val)) 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.路径总和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. 路径总和,我依然给出了递归法和迭代法,这种题目其实用迭代法会复杂一些,能掌握递归方式就够了!

今天是长假最后一天了,内容多一些,也是为了尽快让大家恢复学习状态,哈哈。

加个油!

在留言区留下你的思路吧!

-------end-------

我将算法学习相关的资料已经整理到了Github :https://github.com/youngyangyang04/leetcode-master,里面还有leetcode刷题攻略、各个类型经典题目刷题顺序、思维导图看一看一定会有所收获,如果给你有帮助给一个star支持一下吧!

另外因为公众号改版,时间线被打乱,一些精彩文章大家可能错过了。如果感觉这里的文章对你有帮助, 赶紧给「代码随想录」加一个星标吧,方便第一时间阅读文章

往期 精彩回顾





















「代码随想录」期待你的关注!

每天8:35准时推送一道经典算法题目,推送的每道题目都不是孤立的,而是由浅入深,环环相扣,帮你梳理算法知识脉络,轻松学算法!

二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?
刷题可以加我微信!
右边为个人微信,添加时备注:简单自我介绍」+组队题」
我就知道你[在看]



以上是关于二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?的主要内容,如果未能解决你的问题,请参考以下文章

C++翻转二叉树

C++翻转二叉树

终于搞明白了二叉树的递归函数是否需要返回值

刷题日记Day1 | 二叉树

二叉树感悟

二叉树的前中后序遍历(java递归迭代分别实现)