算法训练之递归(二)


♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥

♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥

♥♥♥我们一起努力成为更好的自己~♥♥♥

♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥

♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥
✨✨✨✨✨✨ 个人主页✨✨✨✨✨✨


上一篇博客我们已经对递归算法进行了一个简单的介绍,接下来这篇博客我们将继续进行递归算法的练习,准备好了吗~我们发车去探索递归的奥秘啦~🚗🚗🚗🚗🚗🚗

目录

Pow(x,n)

计算布尔二叉树的值

求根节点到叶子节点数字之和


Pow(x,n)

Pow(x,n)

这是一个我们比较熟悉的问题,也就是求幂,注意这里的底数以及幂数可以是正数也可以是负数~

算法思路

第一步:先找"相同的子问题"

先来看看:原问题能不能拆成和自己一样的更小问题?这个题要求的是计算 x^n。如果我们直接去想 x^n,会觉得它很大,不太好下手;但如果换个角度,把指数拆小,就会发现规律。比如要求 x^10,其实可以先去求 x^5,然后再用 x^5 * x^5 得到 x^10。再比如要求 x^9,也可以先去求 x^4,然后再用 x^4 * x^4 * x 得到 x^9。也就是说,原问题"求 x^n",可以转化成子问题"求 x^(n/2)",而这个子问题和原问题本质上还是同一种问题,只不过指数规模缩小了。所以这道题适合用递归来做,而且还是一种效率很高的递归。

第二步:只写"当前层"的逻辑

我们不去想递归会展开多少层,也不要急着想最后结果是怎么一层层乘回来的,只需要考虑当前这一层该做什么。当前层的任务很明确:先把更小的子问题交给递归函数去完成,也就是先求出 x^(n/2),把这个结果记作 tmp。然后当前层再根据 n 的奇偶性来决定怎么组合答案:如果 n 是偶数,那么当前层直接返回 tmp * tmp;如果 n 是奇数,那么说明还多出来一个 x,所以当前层返回 tmp * tmp * x。因此,当前层逻辑其实就是:先递归求一半,再利用这一半把当前结果拼出来。

第三步:写清楚出口

递归一定要有结束条件。对于这道题来说,最自然的出口就是当 n == 0 的时候。因为任何数的 0 次方都等于 1,所以当递归把指数不断缩小,最后变成 0 时,就不需要再继续往下递归了,直接返回 1 即可。所以递归出口就是:当 n == 0 时,返回 1

注意:

这道题递归思路不难,但是有几个地方特别容易出错。

①不能把同一个子问题重复递归,比如不能直接写成 pow(x, n/2) * pow(x, n/2),因为这样会把相同的计算做两遍,效率会变差。正确做法是先用一个变量把 pow(x, n/2) 的结果存下来,然后复用这个结果。

②题目中的 n 可能是负数,而负数次幂本质上可以转化成正数次幂来做,也就是 x^(-n) = 1 / x^n,所以当 n < 0 时,要先转成 1.0 / pow(x, -n) 的形式。

③还有一个非常重要的细节:当 nint 的最小值时,直接写 -n 会溢出,所以必须先把 n 转成 long long,再去取相反数,也就是写成 -(long long)n,这样才安全。

这个题的递归本质就是:

先递归求出一半的结果,再根据当前指数的奇偶性,把这一半组合成最终答案。

代码实现

cpp 复制代码
class Solution
{
public:
    double pow(double x, long long n)
    {
        if (n == 0)//递归的出口
            return 1;
        double tmp = pow(x, n / 2);//减少递归层次
        return n % 2 == 0 ? tmp * tmp : tmp * tmp * x;
    }
    double myPow(double x, int n)
    {
        return n > 0 ? pow(x, n) : 1.0 / pow(x, -(long long)n);
        //当n是最小值时,-n会超过int范围
    }
};

运行结果

顺利通过~

计算布尔二叉树的值

计算布尔二叉树的值

算法思路

第一步:先找"相同的子问题"

先来看看:原问题能不能拆成和自己一样的更小问题?这个题要求的是"计算一棵布尔二叉树的值"。如果当前节点不是叶子节点,那么它的值并不是直接给出的最终答案,而是要先去计算左子树的值和右子树的值,再根据当前节点的运算符去做运算。也就是说,原问题是"计算整棵树的值",而子问题其实就是"计算左子树的值"和"计算右子树的值"。这两个子问题和原问题本质上是同一种问题,都是"计算某棵布尔二叉树的值",只不过规模变小了。所以这道题非常适合用递归来做。

第二步:只写"当前层"的逻辑

我们不去想整棵树会递归多少层,也不要急着去展开每一个节点的计算过程,只需要关注当前这一层该做什么。对于当前节点来说,如果它不是叶子节点,那么它的任务其实很明确:先递归去求出左子树的布尔值,再递归去求出右子树的布尔值,然后根据当前节点的 val 决定怎么把这两个结果合起来。如果当前节点的值是 2,说明它表示逻辑或,那么当前层就返回"左子树结果 或 右子树结果";如果当前节点的值是 3,说明它表示逻辑与,那么当前层就返回"左子树结果 与 右子树结果"。所以当前层逻辑本质上就是:先递归求左右子树的值,再用当前节点的运算符把它们合并起来。

第三步:写清楚出口

递归一定要有结束条件。对于这道题来说,最自然的出口就是当前节点是叶子节点的时候。因为叶子节点没有左右孩子,它本身的值就是最终的布尔值,不需要再往下递归计算。题目中已经说明,叶子节点的值只可能是 0 或 1,分别表示 false 和 true。所以递归出口就是:当当前节点的左孩子和右孩子都为空时,直接返回 root->val,也就是返回这个叶子节点本身表示的布尔值。

这个题的递归本质就是:

先递归求出左右子树的布尔值,再由当前节点决定是做"或运算"还是"与运算"。

代码实现

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution 
{
public:
    bool evaluateTree(TreeNode* root) 
    {
        //递归出口--叶子节点
        if(root->left == nullptr && root->right == nullptr)
        {
            return root->val;
        }
        //当前层--将左右子树返回的结果进行计算
        if(root->val == 2)
        {
            return evaluateTree(root->left) || evaluateTree(root->right);
        }
        else if(root->val == 3)
        {
            return evaluateTree(root->left) && evaluateTree(root->right);
        }
        return false;//避免报错
    }
};

简单优化

如果大家希望代码可以简单一点,还可以进行优化,优化代码如下:

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution 
{
public:
    bool evaluateTree(TreeNode* root) 
    {
        //递归出口
        if(!root->left && !root->right)
            return root->val;
        //计算左右子树
        int left = evaluateTree(root->left);
        int right = evaluateTree(root->right);
        //返回
        return root->val == 2?left || right : left && right;
    }
};

运行结果

两段代码都运行通过~

求根节点到叶子节点数字之和

求根节点到叶子节点数字之和

算法思路

第一步:先找"相同的子问题"

先来看看:原问题能不能拆成和自己一样的更小问题?这个题要求的是"求根节点到叶节点数字之和"。题目的意思是:从根走到某个叶子节点,沿途经过的数字可以组成一个整数,比如路径 1 -> 2 -> 3 就表示数字 123。如果我们站在当前节点来看,其实不需要一下子关心整条路径最后形成什么数,只需要知道:从根走到当前节点时,前面已经组成了一个什么数。接下来,再把这个数传给左子树和右子树,让它们继续往下拼接即可。也就是说,原问题是"统计整棵树所有根到叶子的数字和",子问题其实就是"统计某个子树中所有根到叶子的数字和",只是这个子树在计算时要带着前面已经形成的数字一起往下走。所以这道题本质上还是同一种问题不断缩小,非常适合用递归来做。

第二步:只写"当前层"的逻辑"

我们不去想整棵树会递归展开多少层,也不要急着把所有路径一下子列出来,只需要关注当前这一层该做什么。当前层的任务其实很明确:先根据传进来的前缀和 presum,更新当前路径表示的数字。更新方式就是 presum = presum * 10 + root->val,因为原来的数字要整体左移一位,再把当前节点的值拼到末尾。更新完之后,如果当前节点不是叶子节点,那么说明后面还有路要走,这时当前层只需要把新的 presum 分别传给左子树和右子树,让它们继续去计算各自路径的结果,最后再把左右子树返回的和加起来即可。所以当前层逻辑本质上就是:先更新当前路径数字,再把这个结果交给左右子树继续处理,最后汇总左右子树的答案。

第三步:写清楚出口

递归一定要有结束条件。对于这道题来说,最自然的出口就是走到叶子节点的时候。因为一旦走到叶子节点,说明从根到当前节点这条路径已经完整结束了,此时 presum 就正好表示这条路径对应的那个数字,不需要再继续往下递归了,直接把这个数字返回即可。所以递归出口就是:当当前节点既没有左孩子,也没有右孩子时,直接返回当前路径组成的数字 presum

注意

这道题整体递归思路比较清晰,但有几个细节需要特别注意。首先,路径数字的更新方式不是简单地相加,而是要先乘 10 再加当前节点值,也就是 presum = presum * 10 + root->val,因为这是在"拼数字",不是在"求节点值之和"。其次,叶子节点才代表一条完整路径的结束,所以只有走到叶子节点时,才能把当前路径形成的数字作为结果返回;如果当前节点还不是叶子节点,就不能提前返回。再有一点,这道题用的是"参数传递"的递归写法,也就是把前缀数字 presum 一层层往下传,这种写法在树上路径题里非常常见,以后遇到"从根到当前节点积累某种状态"的题,都可以优先想到这种思路。最后,sumNumbers 里直接调用 dfs(root, 0) 就行,因为一开始根节点之前还没有任何数字,所以前缀值应该初始化为 0

这个题的递归本质就是:

把"从根到当前节点形成的数字"一层层往下传,走到叶子节点时返回这条路径对应的数字,最后把所有路径结果加起来。

实现代码

cpp 复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution
{
public:
    int dfs(TreeNode* root, int presum)
    {
        presum = root->val + presum * 10;
        //递归出口--叶子节点-->返回当前分支计算结果
        if (!root->left && !root->right)
            return presum;
        //统计左右子树和返回
        int ret = 0;
        if (root->left)
            ret += dfs(root->left, presum);
        if (root->right)
            ret += dfs(root->right, presum);
        return ret;
    }
    int sumNumbers(TreeNode* root)
    {
        return dfs(root, 0);
    }
};

运行结果

顺利通过~


♥♥♥本篇博客内容结束,期待与各位优秀程序员交流,有什么问题请私信♥♥♥

♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥

✨✨✨✨✨✨个人主页✨✨✨✨✨✨


相关推荐
无限进步_2 小时前
【C++】反转字符串的进阶技巧:每隔k个字符反转k个
java·开发语言·c++·git·算法·github·visual studio
Fly Wine2 小时前
Leetcode只二叉树中序遍历(python解法)
算法·leetcode·职场和发展
bnmoel2 小时前
C语言自定义类型:联合和枚举
c语言·开发语言·数据结构·算法
计算机安禾2 小时前
【数据结构与算法】第34篇:选择排序:简单选择排序与堆排序
c语言·开发语言·数据结构·c++·算法·排序算法·visual studio
汀、人工智能11 小时前
[特殊字符] 第40课:二叉树最大深度
数据结构·算法·数据库架构·图论·bfs·二叉树最大深度
沉鱼.4411 小时前
第十二届题目
java·前端·算法
大熊背12 小时前
ISP Pipeline中Lv实现方式探究之三--lv计算定点实现
数据结构·算法·自动曝光·lv·isppipeline
西岸行者12 小时前
BF信号是如何多路合一的
算法
大熊背13 小时前
ISP Pipeline中Lv实现方式探究之一
算法·自动白平衡·自动曝光