【C语音】——函数递归
一、 什么是递归
1.1 递归是什么
学习C语言函数,递归是永远绕不过去的一个话题,那么。什么是递归呢?
递归其实是一种解决问题的方法,在C语言中,递归就是自己调用自己
。
下面列举一个最简单的C语言递归代码:
#include<stdio.h>
int main()
{
printf("hello world\t");
main();
return 0;
}
上述代码即是main函数不断调用main函数,即不断自己不断调用自己。但上面的代码只是为了演示递归的基本形式,不是为了解决问题。因为代码不断自己调用自己,一直无法停止,所以该代码最终会陷入死循环,导致栈溢出1(Stack overflow)。
注:Stack overflow
同时也是一个程序员技术问答社区网站
1.2 递归的思想
把一个复杂的问题转换为由多个小问题的组成,同时,分割的无数个小问题又与原问题类似,如此来进行求解;当小问题不能再被拆分,问题解决,递归结束。递归的思想就是大事化小,小事化了
。
递归中的递就是递推
的意思,归就是回归
的意思。
- 递:程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。
- 归:触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。
1.3 递归的必要条件
相信大家看了前面的例子,都会意识到:递归不能让他自己乱跑,必须有一个限制条件,不然程序容易陷入死循环。
其实,限制条件正是递归的其中一个必要条件。
递归的两个必要条件:
- 递归存在
限制条件
,当满足这个限制条件时,递归终止。- 每一次递归之后,需
越来越接近
这个限制条件。
二、 递归举例
2.1、例一: 计算 N 阶乘
计算
n
n
n 的阶乘,用以前学过的知识想必大家都会
参考代码:
#include<stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
int sum = 1;
int i = 1;
for (i = 1; i <= n; i++)
{
sum *= i;
}
printf("%d\n", sum);
return 0;
}
那么运用递归的方法又该怎么写呢?
我们都知道,阶乘的公式:
n
!
=
n
∗
(
n
−
1
)
n! = n*(n-1)
n!=n∗(n−1),同理,即可退出
(
n
−
1
)
!
=
(
n
−
1
)
∗
(
n
−
2
)
(n-1)! = (n-1)*(n-2)
(n−1)!=(n−1)∗(n−2)。
那么,递归的思路就有了,当
n
=
=
0
n==0
n==0 时,结果为 1 ,而当
n
>
1
n>1
n>1 时,都可以通过公式进行递推。
图示:
参考代码:
#include<stdio.h>
//递归函数
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 n>0
- 限制条件: n = 0 n = 0 n=0
- 每递推一次, n n n减一,不断逼近限制条件
画图推演:
2.2、 例二:逆序打印一个整数的每一位
用以前的知识,我们可以这样写:
#include<stdio.h>
int main()
{
int n = 0;
scanf("%d", &n);
while (n)
{
printf("%d ", n % 10);
n /= 10;
}
return 0;
}
其实,运用以前的知识,不难发现里面也隐藏着递归的思想:当该数为个位数时,打印他自身;当不是个位数,打印他最后一位数,并让他的位数不断减小就行了。
参考代码:
#include<stdio.h>
void Print(int n)
{
if (n < 10)
printf("%d ", n);
else
{
printf("%d ", n % 10);
return Print(n / 10);
}
}
int main()
{
int m = 0;
scanf("%d", &m);
Print(m);
return 0;
}
当然,Print函数还可以如下简化:
void Print(int n)
{
printf("%d ", n % 10);
if(n >= 10)
{
return Print(n / 10);
}
}
图片演示:
三、 递归与迭代
3.1、 递归的缺点
递归是一种解决问题的好方法,但我们也需要了解递归的缺点,防止递归被误用。
像前面的求
n
n
n 的阶乘:在C语言中,函数的每一次调用,都用为函数申请一块空间,该空间称为函数栈帧。
而函数递归,在递推阶段时,函数每一次调用自身都用像内存申请空间,如果函数不返回,这些空间会被一直占用,直到回归阶段,函数才开始返回,才开始由深到浅逐层释放栈帧空间。
当递归的层次太深,就会浪费太多栈帧空间
,也有可能引起栈溢出问题(stack overflow)。
事实上,递归解决问题的效率并不高。
3.2、 用递归解斐波那契数列
下面我们用一个例子可以更加形象理解这一点:
求第
n
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);
printf("%d\n", Fib(n));
return 0;
}
但是,执行代码我们发现:当输入一个较大的数时,计算结果要很长时间才能求出来,这表明计算过程太过冗长,效率太低
。
为什么会这么慢呢?我们来统计一下计算 n = = 40 n == 40 n==40 时, F i b ( 3 ) Fib(3) Fib(3)计算了多少次
#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);
printf("结果 = %d\n", Fib(n));
printf("计算了%d次\n", count);
return 0;
}
运行结果:
如图,仅仅是
n
=
=
3
n == 3
n==3就计算了这么多次,那还有
4
,
5
,
6
⋅
⋅
⋅
⋅
⋅
⋅
4, 5, 6······
4,5,6⋅⋅⋅⋅⋅⋅可想而知计算过程是有多么冗余
了。
为什么会这样呢?
图解:
注:虽然这代码进行了许多重复计算,但并没有栈溢出,因为他的层次并不是特别深,当 n = = 50 n == 50 n==50 时,层次最多50层。
3.3、 什么是迭代
迭代的定义
迭代是重复反馈过程的活动,其目的通常时为了逼近所需目的或结果。每一次对过程的重复称为一次迭代,而每一次迭代得到的结果会作为下一次迭代的初始值。
可以说迭代就是一种不断用变量的旧值递推出新值得过程
,一般会用一个计数器来判断是否结束迭代。
在现阶段的学习中,我们可以简单地认为迭代通常就是循环
。
3.4、 用迭代解斐波那契数列
解题思路:
我们都知道,斐波那契数列前两个数是
1
1
1 ,后面的数是前两个数的相加,那么只要我们从前往后,不断更新前两个数的值,从小到大计算
就行了。
参考代码:
#include<stdio.h>
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 m = 0;
scanf("%d", &m);
int ret = 0;
ret = Fib(m);
printf("%d\n", ret);
return 0;
}
用迭代的方法去实现这个代码,效率就要高很多了。
3.5、 迭代与递归总结
可能我们看到的许多问题都是用递归的思想进行解释的,是因为递归思想理解起来更容易、更清晰。但事实上,递归解决问题的效率并不高。
有时候,写一个代码,虽然递归难以想到,但是使用递归写出的代码会非常简单,往往一个代码使用递归可能就是几行代码,而写成非递归(迭代)的方式,就得十几行甚至几十行代码。因此递归实现的这种简洁性,就弥补了效率低下的缺点。
但如果递归的不恰当书写,会导致一些无法接受的后果,我们应该放弃使用递归,使用迭代来解决问题。
后期学习数据结构的时候,经常会使用递归
在面试(笔试)的时候,你使用递归很快的解决了代码问题,但遇到“变态”的面试官,想考察你的代码能力,就会让你改成非递归(迭代)~
好啦,本期关于函数递归就介绍到这里啦,希望本期博客能对你有所帮助,同时,如果有错误的地方请多多指正,让我们在C语言的学习路上一起进步!
栈溢出:每一次函数的调用都会向内存(栈区)申请空间来保存函数调用期间各种变量的值,称为:函数栈帧,而内存是有限的,超出内存的存储空间则为栈溢出。 ↩︎