一、什么是递归
在C语⾔中,递归就是:函数⾃⼰调⽤⾃⼰
写一个最简单的递归:
int main()
{
printf("hehe\n");
main();
return 0;
}
运行结果:
上述就是⼀个简单的递归展示,最后会进入死递归,导致栈溢出。
1、递归的思想
大事化小:把⼀个⼤型复杂问题层层转化为⼀个与原问题相似,但规模较⼩的⼦问题来求解,直到⼦问题不能再被拆分,递归就结束了。
二、递归
递归在使用的时候,有2个条件:
①:递归存在限制条件,当达到这个限制条件的时候,递归便不再继续。
②:每次递归调⽤会越来越接近这个限制条件。
练习1:求n的阶乘
n! = n ∗ (n − 1)!
这样的思路就是把⼀个较⼤的问题,转换为⼀个与原问题相似,但规模较⼩的问题来求解。
n的阶乘的递归公式如下:
#include<stdio.h>
int Fact(int n)
{
//限制条件
if (n == 0)
{
return 1;
}
//递归
else
{
return n * Fact(n - 1);
}
}
int main()
{
int num;
scanf("%d", &num);
int ret = Fact(num);
printf("%d的阶乘是%d\n", num, ret);
return 0;
}
运⾏结果:
练习2:顺序打印整数的每⼀位
例如输⼊:1234 ,输出:1 2 3 4。
#include<stdio.h>
void Print(int n)
{
//限制+递归
if (n > 9)
{
Print(n / 10);
}
printf("%d ", n % 10);
}
int main()
{
int num;
scanf("%d", &num);
Print(num);
return 0;
}
运行结果:
三、迭代
其实在C语⾔中每⼀次函数调⽤,都需要为本次函数调⽤在栈区申请⼀块内存空间来保存函数调⽤期间的各种局部变量的值,这块空间被称为运⾏时堆栈,或者函数栈帧。
如果函数不返回,函数对应的栈帧空间就⼀直占⽤,所以如果函数调⽤中存在递归调⽤的话,每⼀次递归函数调⽤都会开辟属于⾃⼰的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采⽤函数递归的⽅式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出的问题。
所以如果不想使⽤递归就得想其他的办法,通常就是迭代的⽅式。
1、迭代的使用
例如:计算n的阶乘时,不使用递归,使用迭代:让1~n的数相乘起来。
#include<stdio.h>
int Fact(int n)
{
int sum = 1;
for (int i = 1; i <= n; i++)
{
sum *= i;
}
return sum;
}
int main()
{
int num;
scanf("%d", &num);
int ret = Fact(num);
printf("%d\n", ret);
return 0;
}
运行结果:
上述代码使用迭代的效率是⽐递归更高的。
但是当⼀个问题⾮常复杂,难以使⽤迭代的⽅式实现时,此时递归简洁性便可以补偿它所带来的运⾏时开销。
2、练习3:求第n个斐波那契数
我们也能举出更加极端的例⼦,例如计算第n个斐波那契数,是不适合使⽤递归求解的,但是斐波那契数的问题通过是使⽤递归的形式描述的。
如图:
看到这公式,很容易诱导我们将代码写成递归的形式,如下所⽰:
#include<stdio.h>
int Fib(int n)
{
//限制条件
if (n <= 2)
{
return 1;
}
//递归
else
{
return Fib(n - 1) + Fib(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
当我们n输⼊为50的时候,需要很⻓时间才能算出结果,说明递归的写法是⾮常低效的,那这是为什么呢?我们用图片来展示,如下:

可以看其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计算,⽽且递归层次越深,冗余计算就会越多。我们可以做个测试:
#include<stdio.h>
int count = 0;
int Fib(int n)
{
//统计第三个斐波拉契数计算的次数
if (n == 3)
{
count++;
}
if (n <= 2)
{
return 1;
}
else
{
return Fib(n - 1) + Fib(n - 2);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("count被计算的次数:%d\n", count);
return 0;
}
运行结果:
这⾥我们看到,在计算第30个斐波那契数的时候,第3个斐波那契数就被重复计算了317811次,这些计算是⾮常冗余的。
所以斐波那契数的计算,使⽤递归是⾮常不明智的,我们就可以通过迭代的⽅式来解决。
如图:
#include<stdio.h>
int Fact(n)
{
int a = 1;
int b = 1;
int c;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int main()
{
int n;
scanf("%d", &n);
int ret = Fact(n);
printf("%d", ret);
return 0;
}
运行结果:
可以看到即使计算第40个斐波拉契数,计算速度也是非常快的,比使用递归的效率高得多。
所以说递归和迭代的使用是要根据实际情况的。
四、拓展学习:
1、⻘蛙跳台阶问题
问题描述:一只青蛙每次可以跳1级或2级台阶,问青蛙跳上n级台阶有多少种不同的跳法。
如图:
那么可以将其列为一个数列:1 2 3 5 …
可以发现其实隐含了斐波拉契数的规律:
在前面已经讲解过使用递归计算斐波拉契数的弊端,因此在这里我们依旧使用迭代。
思路:和计算斐波拉契数一致,只需要将b初始化为2即可:int b = 2;
。
2、汉诺塔问题
问题描述:有三根杆A、B、C,A杆自下而上由大到小放置n个盘子,目标把 A 杆上的所有盘子放在 C 杆上,一次只能移动一个盘子。



思路:我们分为三步,首先初始化A(不是A杆)为起始杆,B为过渡杆,C为目标杆。
①:把A杆上面的n-1个盘子(视为整体)移到B杆 ,此时起始杆、过渡杆、目标杆分别对应A杆 、C杆、B杆。( 递归)
②:起始杆的底盘移到目标杆,打印过程。
③:把B杆上面的n-1个盘子移到C杆 此时起始、过渡、目标杆分别对应B杆 、A杆、C杆。( 递归)
#include<stdio.h>
//A B C的位置分别对应起始杆 过渡杆 目标杆
void Hanoi(int n, char A, char B, char C)
{
//限制条件
if (n > 0)
{
//第一步:把A杆上面的n-1个盘子移到B杆 此时B杆为目标杆
Hanoi(n - 1, A, C, B);
//第二步:底盘从起始杆移到目标杆
printf("%c-->%c\n", A, C);
//第三步:把B杆上面的n-1个盘子移到C杆 此时B杆为起始杆,C杆为目标杆
Hanoi(n - 1, B, A, C);
}
}
int main()
{
int n;
printf("请输入A杆上的盘子个数:");
scanf("%d", &n);
char A = 'A';
char B = 'B';
char C = 'C';
Hanoi(n, A, B, C);
return 0;
}
运行结果: