1.什么是递归
一个函数调用它本身就是递归。递归通常把一个大型复杂的问题层层转化为子问题,直到到子问题无需进一步递归就可以解决的地步。递归极大地降低了代码量。
通常来讲一个递归算法由以下部分组成:
- 能够不使用递归来产生答案的终止方案
- 可将所有其他情况拆分到基本案例
2.递归的作用
递归算法在以下情况下有着很大的作用:
- 数据的定义是按递归定义的
- 问题解法按递归算法实现
- 数据的结构形式是按递归定义的
3.递归的应用
以上文中递归适用的三种情况来帮助大家对递归有一个更好的理解
3.1 当数据的定义是按递归定义的时
最常见的就是Fibonacci数列
F~0~ = 0 ; F~1~ = 1; F~n~ = F~n-1~ + F~n-2~ , n$\geq$ 2
比如说我们现在要求出Fibonacci数列的第5个数字,根据上文提到的递归的组成部分,我们将递归函数的组成分为两部分:
- 当传入的实参是0时返回0,当传入的实参时1时返回1(终止方案)
- 其他情况函数递归调用自己(拆分)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-flwOuEed-1644237481659)(C:\Users\lemer\Desktop\digui.png)]
递归算法会将f(5)逐步拆分成f(1) + f(0) + f(1) + f(1) + f(0) + f(1) + f(0) + f(1)
写成程序是这样的:
#include<iostream>
using namespace std;
int Fibonacci(int n) {
if (n == 0) //终止方案
return 0;
if (n == 1) //终止方案
return 1;
return Fibonacci(n - 1) + Fibonacci(n - 2); //拆分
}
int main() {
cout << Fibonacci(5);
return 0;
}
3.2 问题解法按递归算法实现时
思考下面的问题:把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放, 问共有多少种不同的分法?(5,1,1和1,5,1 是同一种分法。)
问题分析:
1、确定参数:题目有效数据只有苹果和盘子,在构造递归函数时不妨设m个苹果放在n个盘子里放法总数是f(i, j)
2、终止方案:(1)当没有苹果时所有盘子为空,返回1;(2)当没有盘子时返回0
3、拆分:(1)当苹果数m小于盘子数n时必定有盘子为空,且空盘子数至少为n - m,此情况的结果等同与将m个苹果放入m个盘子;(2)当苹果数m大于等于盘子数n时分两种情况:有盘子空和没有盘子空。表示有盘子空可以在递归时将盘子的数量减1但苹果数不变;表示没有盘子空可以先将每个盘子里预放一个苹果,再在递归调用函数时盘子数量不变,但是苹果数量变成原苹果数量减盘子数量。
写成程序是这样的:
#include<iostream>
using namespace std;
int f(int m, int n) {
if (m == 0) //终止方案
return 1;
if (n == 0) //终止方案
return 0;
if (n > m) //拆分
return f(m, m);
return f(m, n - 1) + f(m - n, n); //拆分
}
int main() {
int m, n;
cin >> m >> n;
cout << f(m, n) << endl;
return 0;
}
3.3 数据的结构形式是按递归定义的时
如二叉树的遍历、广度优先搜索等问题
现在给定一个二叉树,要求找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
问题分析:
(1)终止条件:遍历到的节点是空节点
(2)拆分:向当前节点的左子树和右子树遍历,每遍历一次深度加一,每次返回值取其左子树和右子树的最大深度值
#include<iostream>
using namespace std;
typedef struct BTree
{
int value;
struct BTree* lchild;
struct BTree* rchild;
}BTree;
BTree* CreateBTree(BTree* node, int* num, int& index)
{
if (num[index] == 0)
return NULL;
else
{
node = new BTree;
node->value = num[index];
node->lchild = CreateBTree(node->lchild, num, ++index);
node->rchild = CreateBTree(node->rchild, num, ++index);
}
return node;
}
void preOrder(BTree* root)
{
if (root == NULL)
return;
cout << root->value << " ";
preOrder(root->lchild); //递归 左子树
preOrder(root->rchild); //递归 右子树
}
int getdepth(BTree* root)
{
if (root == NULL) //终止条件
return 0;
int lchild_depth = getdepth(root->lchild);
int rchild_depth = getdepth(root->rchild);
return max(lchild_depth, rchild_depth) + 1; //拆分
}
int main()
{
int num[] = { 1,2,4,8,0,0,9,0,0,5,10,0,0,11,0,0,3,6,12,0,0,13,0,0,7,14,0,0,15,0,0 };
BTree* root = NULL;
int index = 0;
root = CreateBTree(root, num, index);
cout << "前序遍历为:\n";
preOrder(root);
cout << endl;
cout << "最大深度为: ";
cout << getdepth(root);
return 0;
}
4.递归的优缺点及其优化
4.1 优点
在一些特定问题中递归比循环更简洁,递归像一个黑盒子,编写时只需要关注边界条件和拆分公式
4.2 缺点
(1)递归是对函数的调用,需要占用栈空间,时间和空间消耗较大
(2)如果不对递归函数做一定的记忆化处理的话会有很多的重复运算,浪费时间和空间
(3)调用栈可能会溢出
4.3 优化(记忆化)
我们看到3.1的例子代码,当n取一个很大的值时会让程序做很多次重复的计算,我们这时可以采用空间换取时间的思想,用一个表记录算出来过的值,每次运算时先在表里找该值有没有被算过,如果有直接用之前算出来的值,如果没有就算出来后将其存入表中
这时代码实现:
class Solution {
public:
unordered_map<int, int>hash; //空间换时间
int fib(int n) {
if(n == 0) return 0;
if(n == 1) return 1;
if(hash[n]) return hash[n]; //算之前查找有没有被算出来过
return hash[n] = fib(n - 1) + fib(n - 2); //没有的话把算出来的值存起来
}
};
5.总结
看到了这里的话相信大家对递归有了一个更好的理解,这里有两个题目可以帮助加深大家对递归的理解:
力扣 70 爬楼梯;力扣 104 二叉树的最大深度
今天的分享就到这里,我们下期再见!