前言
在前面的几篇文章中,我们了解并使用了一些函数的基本用法,但函数除了能够被主函数调用以外,其实还能被自己或是其他的函数调用,而这种特殊的用法就是我们今天讲到的——函数的递归调用
1.递归调用的含义
递归是函数的一种特殊用法,其在C语言中的含义就是自己调用自己。(“递”:递进,“归”:回归)
其中最形象的例子就是:
现在我们就举一个例子:
#include<stdio.h>
int main()
{
printf("hehe\n");
main();//在main函数中又调用了main函数
return 0;
}
//由于在这串代码中没有限制条件,故会一直叠加调用main函数,无限重复的调用自己
//死循环,最后会崩溃(栈溢出)
由此,我们知道如果在递归调用函数的时候,不给施加限制条件那么就有可能出问题。
(栈溢出:即函数在实行的时候用的空间大于了内存分配给他的空间)
递归的思想
把一个比较复杂的问题,层层简化转化为一个与原问题相似但是计算量更小的问题,直到子问题无法被拆分,那么递归就结束了,其实递归就是“大事化小”的过程
2.递推的限制条件
刚才的那个代码由于没有写限制条件,所以一直“递",而没有”归“,而导致了栈溢出的问题。那么我们应该如何规避这一问题呢?——设置限制条件
限制条件包括两个方面:①在满足这个限制条件以后,递归就不再继续(必要条件,没有不行,有了也不一定成立)②每次归调用以后会越来越接近这个限制条件
3.关于递归调用的应用举例
3.1求n的阶乘(不考虑栈溢出的情况)
阶乘公式:n!=(n-1)!*n
思路:此处我们找到了n与n-1之间的递推关系,我们可以通过求(n-1)!来求n!,以此类推,我们求(n-1)!就可以通过求(n-2)!,一直细分下去,每次剥一个数字下来,最后在一次穿上即可。
背后的逻辑:由于递推公式的特性,前一项与后一项的关系与后一项与后一项之间的关系的形式相似,因此可以不断调用同一个结构相同的函数。通过求后一项的结果来”归“前一项的表达式当中从而得到前一项的值,把一个复杂的运算逐步细分,在把最后得到的结果依次向上带入。
//我们假设Fact()就是实现阶乘的函数,那么以上的思路就可以表达为:
Fact(n)=Fact(n-1)*n
//此处的Fact(n)就是n!,而Fact(n-1)就是(n-1)!
//每次使n-1,当n=1的时候就不在调用,那么这样就越来越接近限制条件了
注:由于在这里我们出现了n-1的情况,即n>=2,那么我们就分为n=1和n>=2的情况
即:Fact(n)=①1,n<=0②n*Fact(n-1),n>=2
注:我们这里的情况①并没用n=1,而是n<=0: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 ret=Fact(n);
printf("%d",ret);
return 0;
}
我们可以通过这个图,对于递归调用的代码实现有更加深入的了解:先由地推表达式对于目标进行拆分,拆到可计算的部分,在每次算一点,依次”归“上去即可。
3.2 顺序打印一个整数的各个位数
描述:输入一个整数,从高位到低位,依次打印各位的数字。
eg:输入:1234,输出:1 2 3 4
那么如何做呢?
输入一个整数,计算机无法得知输入的整数有多少位,但如果输入的整数只有一位的时候,由num/10==0,即可判别只有一位,那么我们只用减少输入的整数的位数,直至仅剩一位的时候,即可计算了。
实现的逻辑分析:①打印出整型中的各位数字②把答应出的数字倒序输出
①如何打印出一个整数的个位数:
技巧1:如何求最后一位:num%10(num>=0)的结果即为个位数
技巧2:如何向后推进:(即如何摒弃个位)num/10
eg:print(1234)=4+print(123)(1234%10+1234/10,把4剥离出来,先层层剥离,最后再倒序依次打印)=4+(3+print(12))........,相似的结构,重复使用(把较大的数据,层层细分为可知的数据)
具体的代码是实现:
void Print(int n)
{
if(n>9)
{
Print(n/10);//每次减少1位,再把结果作为形参,再次传给n,再继续。
}
printf("%d",n%10);//直到只有一位时,才依次输出
/*else prinf("%d",n);//n<=9,即此时输入的整数只有一位,不用计算,本身即为其最后一位*/
//这一步其实不用,只有一位,其实位有多位,递归结束时的那种情况
}
#include<stdio.h>
int main()
{
int m=0;
scanf("%d",&m);
Print(m);
return 0;
}
当输入的数不是个位数的时候,会进入if当中,不断的递归,只有当不在满足n>9的条件时才跳出,开始执行递归调用以外的命令。
注:递归调用只会在每次的函数调用完成后,才会返回这一层的结果。先把里层的函数执行完成后再逐渐向后扩展
4.慎用函数的递归
程序在运行的时候会在栈区开辟一块空间来加载数据(运行时的堆栈或函数栈帧),局部变量就存储在栈区当中,当程序运行出某个局部变量所在的”{}“时,其在栈区中开辟的空间就被取消了。
而对于递归调用也类似,递归在”递(递推)“的过程中,函数一直未返回,为函数分配的栈帧空间就一直被占用,每深入一层都会为自己开辟一块栈帧空间。
直至”递“结束,在”归“的时候在逐层取消其开辟的栈帧空间(回收),那么如果递归的次数越多,那么占用的空间就越多。
如果函数的层次太深,就会一直开辟空间,而栈区的空间又有限,就会导致栈溢出。
用递归来解决问题,虽然用的代码较少,但容易栈溢出,当递归的次数比较多的时候,我们通常用迭代(循环)来代替。
并且使用迭代的效率往往高于函数递归,而当一个问题比较复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来运行时的开销。
下面我们就以兔子数列(斐波那契数列)为例,
f1=1,f2=1,fn=fn-1+fn-2(这恰是一个递归表达式)由此我们用递归能轻易的实现,代码如下:
int f(int n)
{
if(n<=2)
renturn 1;
else return f(n-1)+f(n-2);
}
#include<stdio.h>
int main()
{
int n=0;
scanf("%d",&n);
int ret=f(n);
printf("%d\n",ret);
return 0;
}
此时,对于一个比较大的数,就会花费计算机比较长的时间去计算,这不利于我们平时的效率。但这是为什么呢?
其实在这次的运算中,存在很多次重复的计算,
f(50)=f(49)+f(48),而f(49) f(48)为未知,有分别拆分为f(48)+f(47)和f(46)+f(47),由此,按此规律,①计算f(50)就要计算2^49次,太慢。②从第二次计算我们就能看出,在求f(49) f(48)时均包含了f(47)那么f(47)即其后的内容就多算了一次,等等。所以在递归调用当中存在有许多冗余的计算。
下面我们通过一个具体的例子来看一下,使用递归在其层次比较深时,计算的重复度有多高。
#include<stdio.h>
int count=0
int f(int n)
{
if(n==5)
count++
if(n<=2)//前两项不用计算,直接为1,1
return 1;
else
f(n)=f(n-1)+f(n-2);
}
int main()
{
int n=0;
scanf("%d",&n);
int ret=f(n);
printf("%d",count);
return 0;
}
然而当我用迭代(循环)来实现的时候,就会快的多。
思维逻辑:1,1,2,3,5........
a b
①斐波那契数列的第n项,即为前两项的和。
②当一共只有3项的时候,我们计算第三项的方法是把前两项加起来即得到第三项。
③而我们现在想要得到第n项,只用把a,b依次向后移动即可
那么我们怎么让a,b顺次移动呢?————中间量法
我们在a,b以外再定义一个变量,用于保存他们的和,作为新的b,再把b作为新的a即可。
即:
int f(int n);
int a=1;
int b=1;
int c=1;
while(n>2)
{
c=a+b;
a=b;
b=c;
n--;
}
return 0;
return c;
}
此时我们就只用计算n-2项,大大提升了效率
关于何时使用递归,又何时使用迭代的几点建议:
①如果用递归能简单方便的写出代码,而有没有什么明显的缺陷,且运行正常时使用
②如果再用递归时,存在明显的缺陷,如:栈溢出、效率低下,此时务必更换为其他方法
5.两个经典的函数递归问题
经典的汉诺塔问题,青蛙跳台阶问题,都是函数的递归的典型应用而展开的。
青蛙跳台阶问题:一共有n阶台阶,青蛙一次能跳1或2阶台阶,问一共有多少种跳法?
其实就可以看为f(1)=1,f(2)=2的斐波那契数列,当我们求第n项时,我们就想,要能跳到这第n阶,那么要么从倒数第二节开跳,要么从倒数第一节开始,即:f(n)=f(n-1)+f(n-2),而f(n-1)与f(n-2)又可以继续细分,然后就是一个标准的求斐波那契第n项(有n阶时的总方法数)的问题了。
汉诺塔问题:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
①A上只有一个的时候,直接移到C
②当有两个及以上的时候,把A柱子上的盘子分为两部分,上面(n-1)个,和最后一个,先把上方的n-1个放在B柱子上,最后一个放在C上,再把B的放到A上再执行相同的操作。
即Hanio(n)=Haino(n-1)+Hanio(1)
后言
当然作为算法的最经典模型之一的斐波那契数列,求其第n项的方法还有很多,如:矩阵快速幂等等,我们将会再后面的文章中提到。
创作不易,求各位看官,给我你们手中免费的点赞,收藏和关注吧❤