目录
- 什么是递归
- 递归的限制条件
- 递归的举例
- 递归与迭代
- 拓展
1. 什么是递归
递归就是函数自己调用自己
下面是一个最简单的递归
#include <stdio.h>
int main()
{
printf("hehe\n");
main();//main函数中⼜调⽤了main函数
return 0;
}
上面的递归只是为了演示递归的形式,不是为了解决问题。会陷入死递归,导致栈溢出(Stack overflow)
递归的思想:
把原问题转化为一个与原问题相似、规模较小的子问题求解,直到子问题不能被拆分,递归就结束。是一个大事化小的过程。
递就是递推的意思,归就是回归
2. 递归的限制条件
递归在书写的时候,有两个必要条件:
- 满足限制条件,递归不再继续
- 每次递归后越来越接近这个限制条件
3. 递归举例
3.1 求n的阶乘
分析怎么实现
5!=5*4*3*2*1
4!= 4*3*2*1
所以 5!可以看做是 5*4!
这样就把原问题化为规模较小的问题
n!=n*(n-1)!
(n-1)!=(n-1)*(n-2)!
当n<=1时,阶乘为1,就可以将n的阶乘分为两个情况:
写出代码:
int Fact(int n)
{
if(n<=0)
return 1;
else
return n*Fact(n-1);
}
测试5的阶乘结果是120,正确
画图推演上面递归的过程:
求5的阶乘,先求4的阶乘,依次减少,直到n为0的时候,有了答案1,然后回归,1的阶乘计算为1,有了1的阶乘,就可以计算2的阶乘,最后计算出5的阶乘
3.2 顺序打印一个整数的每一位
如:
1234 输出:1 2 3 4
520 输出:5 2 0
代码实现:
如果n是一位的话,它的每一位就是自己,超过1位数的话,就要拆分每一位
1234%10可以得到个位的4,然后除以10得到123,就相当于去掉4
然后继续对123%10,就得到了3,再除以10去掉3,以此类推
不断地%和/
但是有个问题,这样得出的顺序是反着的
首先容易取到的是最低位,通过%10就能取到,那写一个Print来打印n的每一位
Print(1234),表示打印1234的每一位,可以拆分为两步
1.Print(1234/10) 打印123的每一位
2.Print(1234%10) 打印4
完成上述两步,就完成了1234每一位的打印
打印123又可以拆分为Print(123/10) 和 Print(123%10)
这样类推下去
Print(1234)
Print(123) +printf(4)
Print(12) +printf(3)
Print(1) +printf(2)
printf(1)
当n只有个位数的时候直接打印就好了,所以条件就是n>9才递推
代码如下:
int Print(int n) {
if(n>9)
Print(n / 10);
printf("%d",n%10);
}
输出结果:
画图推演上述递归过程:
回归的条件是Print(1),满足n<9,所以打印出n%0,也就是1,然后代码执行完回归到上一层,不断往复
4. 递归与迭代
递归往往只有少量的代码,却完成了大量的运算。但很可能被误用
c语言每一次函数调用,都需要在栈区申请一块内存空间来保存函数调用时期各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧
函数不返回,对应的栈帧空间就会一直占用,每一次递归都会开辟对应的空间,直到回归,才逐层释放
递归层次太深,会浪费太多栈帧空间,也可能栈溢出(Stack overflow)
所以不想用递归的话可以用其他方法,也就是迭代的方式(也就是循环),循环是迭代的一种
比如,计算n的阶乘,也可以使用循环的方式:
int Fact(int n)
{
int i = 0;
int ret = 1;
for(i=1; i<=n; i++)
{
ret *= i;
}
return ret;
}
上述代码也可以完成任务,并且比递归的效率更高
用递归的形式解释更加清晰,但很多迭代的往往比递归效率更高
当一个问题很复杂,难以用迭代的方式实现时,此时递归的简洁性可以弥补开销
举例:求第n个裴波那契数
就像计算第n个裴波那契数,不适合用递归求解,但是问题可以使用递归描述:
看到这个图,很容易写成递归形式,如下所示:
int Fib(int n)
{
if(n<=2)
return 1;
else
return Fib(n-1)+Fib(n-2);
}
当我们输入50的时候,需要很长的时间才能得出结果,说明效率是非常低的,为什么呢?
递归会不断展开,在展开的过程中发现存在很多的重复计算,这些计算导致需要的递归层次成指数倍增长,当计算50时,会有很多步骤
我们可以统计一下第3个裴波那契数被计算了多少次:
int cnt=0;
int Fib(int n)
{
if(n==3)
cnt++;
if(n<=2)
return 1;
else
return Fib(n-1)+Fib(n-2);
最后打印出cnt,显示:
在计算40的时候,仅仅是3就重复计算了这么多次,所以用递归是不明智的,我们可以用迭代的方法解决
int fib(int n) {
int a = 1;
int b = 1;
int c = 1;
while (n > 2) {
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
这里计算50的时候很快就算出来了,但是负数,因为结果超出了int的最大范围。所以递归有时候会出现问题
递归和循环的选择:
- 如果用递归写很容易,写出的代码没问题,就用递归
- 如果递归存在明显的缺陷,就用迭代
5. 拓展
- 青蛙跳台阶
问题描述: 青蛙一次可以跳1个也可以跳2个台阶,问跳n个台阶有几种跳法
问题解析:
首先,先分析跳1个台阶,有1种跳法,跳2个台阶,有2种跳法,跳3个台阶,有3种跳法,4个台阶,有5种跳法,将数据排列起来,1,2,3,5。是一个裴波那切数列
近一步分析,跳4个台阶,如果先跳1个台阶,那求剩下3个台阶有3种跳法,如果先跳2个台阶,就是求剩下2个台阶有2种跳法,将这两个相加,就是跳4个台阶有5种跳法。将问题扩展,求n个台阶有几种跳法,就是求(n-1)+(n-2)个台阶的跳法,然后不断将问题细化,最后求解
代码:
int fib(int n) {
if (n == 1)
return 1;
if (n == 2)
return 2;
return fib(n - 1) + fib(n - 2);
}
- 汉诺塔问题
问题描述:
有n个盘子,必须下面的大,上面的小,要求借助B柱,将A柱的所有盘子移动到C柱
例如:
当只有一个盘子的时候,直接移动到C盘
移动过程: A→C
当有两个盘子时
先将A柱上面的移动到B柱,然后将A柱剩下的一个挪到C柱,再将B柱的挪到C柱
移动过程: A→B A→C B→C
当有三个盘子时:
移动过程: A→C A→B C→B A→C B→A B→C A→C
解析思路:
通过三个方块时很明显可以感觉到,思路是通过C柱将除最底下的都移动到B柱,然后将A柱的移动到C柱。然后B柱剩下的也是这个思路,借助A柱将B上面的全部移动到C就完成
代码:
void move(char pos1,char pos2) {
//打印移动字符
printf("%c→%c ", pos1, pos2);
}
void Hanoi(int n,char pos1,char pos2,char pos3) {
if (n == 1)
move(pos1, pos3);
else {
Hanoi(n - 1, pos1, pos3, pos2); //移动n-1个从pos1通过pos3到pos2
move(pos1, pos3); //移动最下层一个到pos3
Hanoi(n - 1, pos2, pos1, pos3); //移动剩下的从pos2通过pos1到pos3
}
}
Hanoi(3,'A','B','C');
测试3个的结果: