递归
在C语言中,我们把函数在运行时自己调用自己的情况叫做递归,递就是传递的意思,归就是回归的意思。递归函数就是调用自身并包含结束递归调用的基本情况或退出条件的函数。
递归的特点:使用少量的代码,完成复杂的任务
简单的函数递归如下
int main ()
{
printf("hehe\n")
main();
return 0;
}
这就是一个最简单的递归,但是这个代码运行起来会进入死循环,会导致栈溢出。所以函数不能这样无限循环下去,递归是必须结束条件
递归的限制条件
- 递归存在限制条件,当满足这个条件时,递归就不再继续
- 每次调用递归函数就会离限制性条件更进一步
通过观察递归函数,不难发现递归的结束条件从某种意义上讲也是起始条件
应用
一个问题有多种解决方法,即递归与非递归(迭代)
具体选择,就要看这个方法是否适合,是否便捷
求n的阶乘即 n!
在没有学习递归的方法时,我们会这样写
int main()
{
int n = 0;
scanf("%d", &n);
int i = 0;
int sum = 1;
for (i = 1; i <= n; i++)
{
sum = sum * i;
}
printf("%d", sum);
return 0;
}
这种方法就是非递归。在我们观察循环的时候,我们发现每一次循环都是 i 乘以 i-1 的阶乘,并且 i 值不断变化,接近判断条件,于是我们也可以把这个函数写成递归的形式
int Fact(int n)
{
if (n == 0)
return 1;
else
return n * Fact(n - 1);
}
int main()
{
int n = 0;
scanf("%d", &n);
int sum = Fact(n);
printf("%d\n", sum);
return 0;
}
这个函数递归的条件是 n>0,停下来的条件是n==0,并且不断逼近跳出条件,所以符合递归条件
递归与非递归的选择
递归是一种很好的编程技巧,但是某些情况下就不适合递归,因为在C语言中每调用一次函数,都需要为本次函数调用在内存的栈区中申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。函数不返回,函数对应的栈帧空间就会一直占用,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采用函数递归方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出的问题因为随着递归的展开就会运行到很多空间,会导致栈溢出。最典型的就是斐波那契数。
斐波那契数列(Fibonacci sequence),又称黄金分割数列。其数值为:1、1、2、3、5、8、13、21、34……这个数列从第3项开始 ,每一项都等于前两项之和。
根据这个数的定义,我们很容易想到运用递归的方法解决问题
int count = 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 ret = Fib(n);
printf("%d\n", ret);
printf("%d\n", count);
return 0;
}
运行发现,当我们输入的值小时,运行速度很快,但是当输入的值过大时,运行出结果的速度会很慢,原因是随着递归函数不断的展开,会有很多重复计算,我们来检验一下
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("%d\n", ret);
printf("%d\n", count);
return 0;
}
当我们输入40时,我们会发现Fib(3)已经计算了39088169次。
随着递归层次越深,冗余的计算就会越多。 所以这个时候我们在选择递归就是不明智的选择,那么我们就要想想非递归方法。
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;
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fib(n);
printf("%d\n", ret);
return 0;
}
这样使用循环的写法,减少了冗余的计算,大大提高了效率。所以递归并不适合所有的地方,在选择方法时,我们要多加考虑。
关于函数的栈帧的问题后续也会专门写一篇来讲述为什么函数递归会栈溢出。尽情期待
以上内容,如果有任何问题,请留言或私信我。期待与大家沟通交流。