目录
1.递归
1.1递归的思想
1.2递归的限制条件
2.递归与迭代
1.递归
-
函数递归是什么?
递归是学习C语⾔函数绕不开的⼀个话题,那什么是递归呢?
递归其实是⼀种解决问题的⽅法,在C语⾔中,递归就是函数⾃⼰调⽤⾃⼰。
写⼀个史上最简单的C语⾔递归代码:
#include <stdio.h>
int main()
{
printf("hehe\n");
main();//main函数中⼜调⽤了main函数 死循环不断的打印hehe
return 0;
}
-
每一次函数调用,都要为这次函数调用分配内存空间是内存的栈区上分配的,如果无限的递归调用函数,就会将栈区空间填满(使用完) ,这时就出现了栈溢出(Stack flow)的现象
1.1递归的思想
把⼀个⼤型复杂问题层层转化为⼀个与原问题相似,但规模较小的⼦问题来求解;直到⼦问题不能再被拆分,递归就结束了。所以递归的思考⽅式就是把⼤事化⼩的过程。
递归中的递就是递推的意思,归就是回归的意思,接下来慢慢来体会
1.2递归的限制条件
递归在书写的时候,有2个必要条件(一定要写):
-
递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
-
每次递归调⽤之后越来越接近这个限制条件。
在下面的例⼦中,我们逐步体会这2个限制条件
题⽬:计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。
当 n==0 的时候,n的阶乘是1,其余n的阶乘都是可以通过公式计算。
n的阶乘的递归公式如下:
-
递归就是:递推,回归的意思,一开始先一步步的递推下去,然后再回归将结果一个个计算得到最终结果
-
少量的代码完成复杂的运算(递归不断的申请内存空间如果不及时结束的话就会溢出)
int Fact(int n) {
if (n == 0) { //n=0限制条件结束条件
return 1; // n=0的时候不再往下拆解 达到限制条件
}
else if (n > 0)
{
return n * Fact(n - 1);
}
else
{
printf("输入的值有误请重新输入:\n");
}
}
int main() {
int n = 0;
scanf("%d", &n);
int ret = Fact(n);
printf("%d", ret);
return 0;
}
-
输⼊⼀个整数m,按照顺序打印整数的每⼀位。
分析:用递归的方法 我们将 1234 按顺序输出 1 2 3 4
我们可以定义一个Print()函数
先递推:(一直递推到最高位,然后再从最高位开始打印,就会按顺序输出)
(1234) 除以十去掉最后一位 (123) 4
(123) 4 ---> (12) 3 4 ----> (1)2 3 4 ---> 1 2 3 4
每次都调用自己,直到不能再分(限制条件)
后回归:
最后当n=1的时候不满足n>9的条件,达到限制条件然后进行回归,
1%10 = 1
12%10=2
123%10 =3
然后再顺序输出1 2 3
int Print(int n) {
if (n > 9)//当n是两位数以上
{
Print(n / 10);
}
printf("%d ", n % 10);
}
int main() {
int n = 0;
scanf("%d", &n);
Print(n);
}
总结:就是写一个限制条件,然后不断的调用自己本身(递推过程),当达到限制条件的时候停止递推,然后回归(不断返回)
2.递归与迭代
递归是⼀种很好的编程技巧,但是和很多技巧⼀样,也是可能被误⽤的,就像举例1⼀样,看到推导的公式,很容易就被写成递归的形式:
int Fact(int n)
{
if(n==0)
return 1;
else
return n*Fact(n-1)
}
Fact函数是可以产⽣正确的结果,但是在递归函数调⽤的过程中涉及⼀些运⾏时的开销。
在C语⾔中每⼀次函数调⽤,都需要为本次函数调⽤在内存的栈区,申请⼀块内存空间来保存函数调⽤期间的各种局部变量的值,这块空间被称为运⾏时堆栈,或者函数栈帧。
涵数不返回,函数对应的栈帧空间就⼀直占用,所以如果函数调用中存在递归调用的话,每⼀次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采⽤函数递归的⽅式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack overflow)的问题
-
我们可以用另外一种更简便的方法,迭代(循环中的一种)
//循环(其中一种循环是迭代)
int Fact(int n) {
int ret = 1; //记得从1开始 如果0的话每次相乘都为0
int i = 0;
for (i = 1; i <= n; i++) {
ret *= i;
}
return ret;
}
int main() {
int n = 0;
scanf("%d", &n);
int r = Fact(n);
printf("%d\n", r);
return 0;
}
我们怎么权衡递归和迭代呢?
-
如果是一个非常复杂的问题,我们可以用简单的递归方法解决,又不会造成栈溢出就使用递归调用
-
如果用递归方法出现很明显的缺陷,造成溢出;就是用迭代的方法
比如:求斐波那契数列,用递归的方法计算,
这个代码当计算很大的数字的时候,速度非常慢,有大量重复的计算
//求斐波拉契
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 r = Fib(n);
printf("%d", r);
return 0;
}
其实递归程序会不断的展开,在展开的过程中,我们很容易就能发现,在递归的过程中会有重复计算,而且递归层次越深,冗余计算就会越多。我们可以作业测试:
int count = 0;
int Fib(int n) {
if (n == 3)
count++;//统计第3个斐波那契数被计算的次数
if (n <= 2)
return 1;
else
return Fib(n - 1) + Fib(n - 2);
}
int main() {
int n = 0;
scanf("%d", &n);
int r = Fib(n);
printf("%d\n", r);
printf("count=%d", count);
return 0;
}
这⾥我们看到了,在计算第40个斐波那契数的时候,使⽤递归⽅式,第3个斐波那契数就被重复计算了39088169次,这些计算是非常冗余的。所以斐波那契数的计算,使⽤递归是⾮常不明智的,我们就得想迭代的⽅式解决。
我们知道斐波那契数的前2个数都1,然后前2个数相加就是第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--;//每次剪到2的时候就退出循环
}
return c;
}
int main() {
int n = 0;
scanf("%d", &n);
int r = Fib(n);
printf("%d\n", r);
return 0;
}
-
有个递归的例子(递归层次太深),会死递归;溢出 等他一直加到3989的时候就会溢出
-
上面斐波拉契函数递归层次不会太深,但是次数冗余
void test(int n) {
printf("%d ", n);
if (n <= 10000) {
test(n + 1);
}
}
int main() {
test(1);
return 0;
}