【C语言】递归详解
目录
- 1.前言
- 2. 递归的定义
- 3. 递归的限制条件
- 4. 递归举例
- 4.1 求n的阶乘
- 4.1.1 分析和代码实现
- 4.1.2 画图演示
- 4.2 顺序打印一个整数的每一位
- 4.2.1 分析和代码实现
- 4.2.2 画图推演
- 4.3 求第n个斐波那契数
- 5. 递归与迭代
- 5.1 迭代求第n个斐波那契数
1.前言
这次博客内容是与递归有关,递归是学习C语⾔函数绕不开的⼀个话题,那什么是递归呢?接下来正⽂开始。
2. 递归的定义
递归其实是一种解决问题的方法,在C语言中,递归就是函数自己调用自己。
来看看一个简单的C语言递归代码
#include <stdio.h>
int main()
{
printf("hehe\n");
main();//main函数中⼜调⽤了main函数
return 0;
}
上述就是⼀个简单的递归程序,只不过上⾯的递归只是为了演示递归的基本形式,不是为了解决问题,代码最终也会陷⼊死递归,导致栈溢出。
递归的思想:
把⼀个大型复杂问题层层转化为⼀个与原问题相似,但规模较小的子问题来求解;直到子问题不能再被拆分,递归就结束了。所以递归的思考方式就是把大事化小的过程。
递归中的递就是递推的意思,归就是回归的意思,接下来慢慢来体会。
3. 递归的限制条件
递归在书写的时候,有2个必要条件:
- 递归存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件
在下面的例子中,我们体会一下这2个限制条件。
4. 递归举例
4.1 求n的阶乘
计算n的阶乘(不考虑溢出),n的阶乘就是1~n的数字累积相乘。
4.1.1 分析和代码实现
将5的阶乘分成4的阶乘乘5;
将4的阶乘分成3的阶乘乘4;
将3的阶乘分成2的阶乘乘3;
将2的阶乘分成1的阶乘乘2;
这样的思路就是把⼀个较大的问题,转换为⼀个与原问题相似,但规模较小的问题来求解的。直到n是1或者0时,不再拆解
最终将n的阶乘就写成n*(n-1)!
直到n是1或者0时,不再拆解
如果将阶乘写成一个函数Fact(n)
,
那么Fact(n)=n*Fact(n-1)
再稍微分析一下,当 n<=1 的时候,n的阶乘是1,其余n的阶乘都是可以通过上述公式计算。
n的阶乘的递归公式如下:
那我们就可以写出函数Fact
求n的阶乘,假设Fact(n)
就是求n的阶乘,那么Fact(n-1)
就是求n-1的阶乘,函数如下:
int Fact(int n)
{
if(n<=0)
return 1;
else
return n*Fact(n-1);
}
来测试一下:
int Fact(int n)
{
if(n<=0)
return 1;
else
return n*Fact(n-1);
}
int main()
{
int n = 0;
scanf("%d", &n);
int ret = Fact(n);
printf("%d\n", ret);
return 0;
}
我们知道3的阶乘就是321=6,结果显然是我们所想的。
4.1.2 画图演示
蓝色是递推的过程,此时并没有开始相乘。
而红色是回归的过程,此时回归时相乘。
4.2 顺序打印一个整数的每一位
输⼊一个整数n,打印这个按照顺序打印整数的每⼀位
输⼊:1234 输出:1 2 3 4
输⼊:521 输出:5 2 1
4.2.1 分析和代码实现
这个题目,放在我们面前,首先想到的是,怎么得到这个数的每一位呢?
1234%10就能得到4,然后1234/10得到123,这就相当于去掉了4;
然后继续对123%10,就得到了3,再除10去掉3,以此类推;
不断的 %10 和 \10 操作,直到1234的每⼀位都得到;
但是这里有个问题就是得到的数字顺序是倒着的。
但是我们有了灵感,我们发现其实⼀个数字的最低位是最容易得到的,通过%10就能得到
那我们假设想写⼀个函数Print来打印n的每⼀位,如下表示:
Print(n)
如果n是1234,那表⽰为
Print(1234) //打印1234的每⼀位
其中1234中的4可以通过%10得到,那么
Print(1234)就可以拆分为两步:
- Print(1234/10) //打印123的每⼀位
- printf(1234%10) //打印4 完成上述2步,那就完成了1234每⼀位的打印 那么Print(123)⼜可以拆分为Print(123/10) + printf(123%10)
以此类推下去,就有
Print(1234)
==>Print(123) + printf(4)
==>Print(12) + printf(3)
==>Print(1) + printf(2)
==>printf(1)
直到被打印的数字变成⼀位数的时候,就不需要再拆分,递归结束。
那么代码就是
void Print(int n)
{
if (n > 9)
Print(n/10);
printf("%d ", n % 10);
}
int main()
{
int n = 0;
scanf("%d", &n);//1234
Print(n);
return 0;
}
在这个解题的过程中,我们就是使用了大事化小的思路
把Print(1234)
打印1234每一位,拆解为首先Print(123)
打印123的每⼀位,再打印得到的4
把Print(123)
打印123每一位,拆解为首先Print(12)
打印12的每一位,再打印得到的3
直到Print
打印的是一位数,直接打印就行。
4.2.2 画图推演
4.3 求第n个斐波那契数
斐波那契数列前两项都是1,后面的是前面两项的和。
斐波那契数的问题通过是使用递归的形式描述的公式。
看到这公式,很容易诱导我们将代码写成递归的形式,如下所示:
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的时候,需要很长时间才能算出结果,这个计算所花费的时间,是我们很难接受的,这也说明递归的写法是非常常低效的,那是为什么呢?
要计算50就要先计算49和48,要计算49就要计算48和47,要计算48就要计算47和46,…一直这个下去,浪费时间重复计算。
那么除了递归还有其它的方式吗?
此时就要介绍迭代。
5. 递归与迭代
递归是一种很好的编程技巧,但是很多技巧一样,也是可能被误用的,就像举例1⼀样,看到推导的公式,很容易就被写成递归的形式:
int Fact(int n)
{
if(n<=0)
return 1;
else
return n*Fact(n-1);
}
Fact函数是可以产生正确的结果,但是在递归函数调用的过程中涉及一些运行时的开销。
在C语言中每一次函数调用,都要需要为本次函数调用在栈区申请⼀块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈,或者函数栈帧。
函数不返回,函数对应的栈帧空间就⼀直占用,所以如果函数调用中存在递归调用的话,每⼀次递归函数调用都会开辟属于自己的栈帧空间,直到函数递归不再继续,开始回归,才逐层释放栈帧空间。
所以如果采用函数递归的方式完成代码,递归层次太深,就会浪费太多的栈帧空间,也可能引起栈溢出(stack over flow)的问题。
所以如果不想使用递归就得想其他的办法,通常就是迭代的方法(通常就是循环的方法)。
比如:计算n的阶乘,也是可以产生1~n的数字累计乘在⼀起的。
int main()
{
int n = 0;
scanf("%d", &n);
int i = 0;
int ret = 1;
for (i = 1; i <= n; i++)
{
ret = ret * i;
}
printf("%d\n", ret);
return 0;
}
上述代码是能够完成任务,并且效率是比递归的方式更好的。
注意:
- 如果一个问题使用递归的方式去写代码,是非常方便的,简单的写出的代码是没有明显缺陷的,这个时候使用递归就可以
- 如果使用递归写的代码, 是存在明显的缺陷的
比如: 栈溢出, 效率低下等
这时候必须考虑其他方式, 比如:迭代
5.1 迭代求第n个斐波那契数
前两个数不需要计算,假设第一个用a记录,第二个用b记录。当n大于2时就要实现前面两个数字,就要相加,然后将a和b都向后挪,也就是将b的值给a,c的值给b,然后再执行a+b,每执行一次n都要减减一下。
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;
}
有问题请指出,大家一起进步!