
♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥
♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥
♥♥♥我们一起努力成为更好的自己~♥♥♥
♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥
♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥
✨✨✨✨✨✨ 个人主页✨✨✨✨✨✨
上一篇博客我们已经对递归算法进行了一个简单的介绍,接下来这篇博客我们将继续进行递归算法的练习,准备好了吗~我们发车去探索递归的奥秘啦~🚗🚗🚗🚗🚗🚗
目录
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) 的形式。
③还有一个非常重要的细节:当 n 是 int 的最小值时,直接写 -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);
}
};
运行结果:

顺利通过~
♥♥♥本篇博客内容结束,期待与各位优秀程序员交流,有什么问题请私信♥♥♥
♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥
✨✨✨✨✨✨个人主页✨✨✨✨✨✨