在C语言中,对于一个函数而言,最大的特征就是可以调用别人,或者被别人调用,从而实现函数,那么我们来思考一个问题,函数可以被自己调用吗?
答案是可以的,一个函数对于自己的调用,这就被称为函数的递归,是一种算法的思想,那么就让我们一起来认识一下函数的递归吧!
函数的递归
递归是有限的:所有递归问题的子问题是可以用相同逻辑去解决;且递归的问题规模会不断缩小,直到缩小成一个很简单的问题,不需要递归便可实现
那么为什么递归一定要是有限的呢?
下面我们来通过几个例子来认识一下递归
1.函数的阶乘
int factorial(int n) {//递归实现
if(n = 1){
return 1;
}
return n* factorial(n-1);
}
***********************
int factorial2(iny n) {//迭代实现
int result = 1;
while(n){
result *=n;
n--;
}
}
我们可以发现,事实上,递归的代码写起来非常简洁,但是却不容易想得到,那么我们如何去深入理解递归呢?
现在,我们根据这个函数的阶乘这个简单的例子来认识一下递归,在这里我们需要进入到内存的角度去剖析递归到底是如何实现的。
这时候我们需要介绍很多的概念,最左边的就是我们所看到的内存(进程地址空间),大致可以化为这个样子,从下往上(地址依次增加)可以分为,代码区,静态常量区,已初始化全局数据区,未初始化全局数据区,堆区以及栈区,而在我们这章,主要来认识下栈区,这里简单介绍下栈,栈具有的最大的特点便是先进后出,以其后进先出,就像羽毛球桶,先被压进去的羽毛球永远最后才能被拿出来,而最后进去的羽毛球永远先被拿出来,我们可以看到,在我们的栈空间中它是可以不断向下拓展的,这是因为我们在实现函数调用的时候,都是在栈上开辟的空间。
在这个图中,我们先调用了main函数,形成了main函数的栈帧,而在main函数中,我们又定义了result变量,所以resule变量便在main函数的栈帧上开辟了。那么当我们调用factorial(递归函数)时,因为栈是从上往下拓展的,所以我们的factorial函数栈帧便在main函数下开辟了,而fact(简写)函数中又定义了临时变量n,这个n便被创建在了main与fact两个栈桢中间(而栈帧大小则是由函数中创建的临时变量个数以及类型而进行估算)此时我们需要明确的概念便是:当函数被调用之时,在栈上依次开辟空间,当调用完毕时,依次退栈(被释放),退回主程序,在这过程中,临时变量在函数被调用时被开辟空间定义,而在函数调用完毕时随着函数一同退栈(被释放),这也就是为什么临时变量具有临时性的原因
那么在函数被调用时底层是如何实现的呢,我们可以通过阅读汇编代码来观察
可以看到,函数的调用就是在进栈移栈出栈的过程,更加印证了函数时在栈中开辟并调用的这一结论
补充:在函数调用,栈帧被创建时,需要开辟空间,这时候便会有时间与空间的耗费,而退出函数,销毁栈帧时同样也需要时间成本,总结起来就是,函数的调用是有成本的,所以不可以无限制调用
有了我们以上的基础概念,那么我们再继续回到我们的递归函数factorial,看看它具体是如何实现的呢?
在我们的fact函数中,当我们输入形参5时,第一个调用的时fact(5),而在我们fact(5)中,却又有fact(4),我们又开始调用fact(4),以此类推,直到调用到fact(1),进入到if中,return了1;此时,我们开始了函数的返回,当fact(1)=1被返回时,进入了fact(2)中,return 了2*1;而此时fact(2)也被返回,返回到了fact(3)中,return 了3*2*1,以此类推,直到返回到fact(5)返回最后的结果.最后回到main函数的调用,保存在result中总结起来就是:调用时从上往下调用(不断形成栈帧),返回时再依次从下往上返回(不断释放栈帧)并完成计算,从规模最小的问题开始返回,直到返回到最大的问题得到结果
这时候我们在思考一下我们最初的那个问题
为什么函数的递归只能是有限递归?答:栈结构是向下增长的,若无限制的递归,就会无限制的形成栈帧,而又因为栈空间有限(最大不能向下拓展占用到堆),就会形成栈溢出报错,所以不能进行无限制的递归
2.求字符串长度
#include <stdio.h>
#include<Windows.h>
int MyStrlen1(const char *s){//利用指针与循环依次判断并计数
int len = 0;
while (*s!='\0'){//迭代实现
s++;
len++;
}
return len;
}
int MyStrlen2(const char *s){//递归实现
if('\0'==*s){
return 0;
}
return 1+MyStrlen(s+1);
int main()
{
char *str = "hello world!";
int len1 = MyStrlen1(str);
int len2 = MyStrlen2(abcd);
printf("%d\n", len1);//12
printf("%d\n", len2);//4
system("pause");
return 0;
}
这便是这道题的递归实现过程,通过将问题不断缩小,最后依次返回
3.无符号整型,打印每一位
#include <stdio.h>
void ShowUint(int n) {
if(n>9)
{
ShowUint(n/10);
}
printf("%d ", n%10);
}
int main()
{
int num = 1234;
ShowUint(num);//1 2 3 4
return 0; }
此题仅用了三行代码便完成了需求,这大大的体现了递归的代码简洁性
4.斐波那契数列(求第n个斐波那契数的值)
int fib(int n) {
if (n == 2||n==1)
return 1;
else
return fib(n - 1) + fib(n - 2);
}
当我们对这个程序进行分析时会发现,在递归调用时他是一个二叉树的结构,中间会有许许多多的重复调用,这大量的浪费了计算机时间与空间内存成本
总的来说:递归本身因天然需要函数调用,势必引起效率低下(时间),甚至栈溢出问题(空间)
递归使用要有度!如果其他的解决方案可以解决,尽量使用其他成本更小的方案
那如何解决这个问题呢,此时我们便可以使用迭代的方法去实现这个斐波那契数列
int Fib(int n){
int first = 1;
int second = 2;
int third = 1;
while (n>2){
third = second + first;
first = second;
second = third;
n--;
}
return third;
}
相对于递归方式而言,迭代版本的斐波那契数组没有重复计算,也没有多次的函数调用,仅在一个栈帧中进行的,所以效率高
通过上面的几个递归例子,相信我们对递归已经有一个初始的认识了吧,但是如果想要深刻理解递归,这就需要我们将语言,操作系统,数据结构预算法等都学完,再加上很多项目的经验,才可以真正的从深层理解递归,递归地学习学无止境,一起加油吧!