当前位置:   article > 正文

函数的使用之递归调用

递归调用

前言

在前面的几篇文章中,我们了解并使用了一些函数的基本用法,但函数除了能够被主函数调用以外,其实还能被自己或是其他的函数调用,而这种特殊的用法就是我们今天讲到的——函数的递归调用

1.递归调用的含义

递归是函数的一种特殊用法,其在C语言中的含义就是自己调用自己。(“递”:递进,“归”:回归)

其中最形象的例子就是:

现在我们就举一个例子:

  1. #include<stdio.h>
  2. int main()
  3. {
  4. printf("hehe\n");
  5. main();//在main函数中又调用了main函数
  6. return 0
  7. }
  8. //由于在这串代码中没有限制条件,故会一直叠加调用main函数,无限重复的调用自己
  9. //死循环,最后会崩溃(栈溢出)

由此,我们知道如果在递归调用函数的时候,不给施加限制条件那么就有可能出问题。

(栈溢出:即函数在实行的时候用的空间大于了内存分配给他的空间)

递归的思想

把一个比较复杂的问题,层层简化转化为一个与原问题相似但是计算量更小的问题,直到子问题无法被拆分,那么递归就结束了,其实递归就是“大事化小”的过程

2.递推的限制条件

刚才的那个代码由于没有写限制条件,所以一直“递",而没有”归“,而导致了栈溢出的问题。那么我们应该如何规避这一问题呢?——设置限制条件

限制条件包括两个方面:①在满足这个限制条件以后,递归就不再继续(必要条件,没有不行,有了也不一定成立)②每次归调用以后会越来越接近这个限制条件

3.关于递归调用的应用举例

3.1求n的阶乘(不考虑栈溢出的情况)

阶乘公式:n!=(n-1)!*n

思路:此处我们找到了n与n-1之间的递推关系,我们可以通过求(n-1)!来求n!,以此类推,我们求(n-1)!就可以通过求(n-2)!,一直细分下去,每次剥一个数字下来,最后在一次穿上即可。

背后的逻辑:由于递推公式的特性,前一项与后一项的关系与后一项与后一项之间的关系的形式相似,因此可以不断调用同一个结构相同的函数。通过求后一项的结果来”归“前一项的表达式当中从而得到前一项的值,把一个复杂的运算逐步细分,在把最后得到的结果依次向上带入。

  1. //我们假设Fact()就是实现阶乘的函数,那么以上的思路就可以表达为:
  2. Fact(n)=Fact(n-1)*n
  3. //此处的Fact(n)就是n!,而Fact(n-1)就是(n-1)!
  4. //每次使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的时候仍会被调用,且是最后一次调用

那么最后的代码实现就是这样的

  1. #include<stdio.h>
  2. int Fact(int n)
  3. {
  4. if(n<=0)
  5. return 1;
  6. else
  7. return n*Fact(n-1);
  8. }
  9. int main()
  10. {
  11. int n=0;
  12. scanf("%d",&n);
  13. int ret=Fact(n);
  14. printf("%d",ret);
  15. return 0;
  16. }

 我们可以通过这个图,对于递归调用的代码实现有更加深入的了解:先由地推表达式对于目标进行拆分,拆到可计算的部分,在每次算一点,依次”归“上去即可。

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))........,相似的结构,重复使用(把较大的数据,层层细分为可知的数据)

具体的代码是实现:

  1. void Print(int n)
  2. {
  3. if(n>9)
  4. {
  5. Print(n/10);//每次减少1位,再把结果作为形参,再次传给n,再继续。
  6. }
  7. printf("%d",n%10);//直到只有一位时,才依次输出
  8. /*else prinf("%d",n);//n<=9,即此时输入的整数只有一位,不用计算,本身即为其最后一位*/
  9. //这一步其实不用,只有一位,其实位有多位,递归结束时的那种情况
  10. }
  11. #include<stdio.h>
  12. int main()
  13. {
  14. int m=0;
  15. scanf("%d",&m);
  16. Print(m);
  17. return 0;
  18. }

当输入的数不是个位数的时候,会进入if当中,不断的递归,只有当不在满足n>9的条件时才跳出,开始执行递归调用以外的命令。

注:递归调用只会在每次的函数调用完成后,才会返回这一层的结果。先把里层的函数执行完成后再逐渐向后扩展

4.慎用函数的递归

程序在运行的时候会在栈区开辟一块空间来加载数据(运行时的堆栈或函数栈帧),局部变量就存储在栈区当中,当程序运行出某个局部变量所在的”{}“时,其在栈区中开辟的空间就被取消了。

而对于递归调用也类似,递归在”递(递推)“的过程中,函数一直未返回,为函数分配的栈帧空间就一直被占用,每深入一层都会为自己开辟一块栈帧空间。

直至”递“结束,在”归“的时候在逐层取消其开辟的栈帧空间(回收),那么如果递归的次数越多,那么占用的空间就越多。

如果函数的层次太深,就会一直开辟空间,而栈区的空间又有限,就会导致栈溢出。

用递归来解决问题,虽然用的代码较少,但容易栈溢出,当递归的次数比较多的时候,我们通常用迭代(循环)来代替。

并且使用迭代的效率往往高于函数递归,而当一个问题比较复杂,难以用迭代实现时,此时递归实现的简洁性便可以补偿它所带来运行时的开销。

下面我们就以兔子数列(斐波那契数列)为例,

f1=1,f2=1,fn=fn-1+fn-2(这恰是一个递归表达式)由此我们用递归能轻易的实现,代码如下:

  1. int f(int n)
  2. {
  3. if(n<=2)
  4. renturn 1;
  5. else return f(n-1)+f(n-2);
  6. }
  7. #include<stdio.h>
  8. int main()
  9. {
  10. int n=0;
  11. scanf("%d",&n);
  12. int ret=f(n);
  13. printf("%d\n",ret);
  14. return 0;
  15. }

此时,对于一个比较大的数,就会花费计算机比较长的时间去计算,这不利于我们平时的效率。但这是为什么呢?

其实在这次的运算中,存在很多次重复的计算,

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)即其后的内容就多算了一次,等等。所以在递归调用当中存在有许多冗余的计算。

下面我们通过一个具体的例子来看一下,使用递归在其层次比较深时,计算的重复度有多高。

  1. #include<stdio.h>
  2. int count=0
  3. int f(int n)
  4. {
  5. if(n==5)
  6. count++
  7. if(n<=2)//前两项不用计算,直接为1,1
  8. return 1;
  9. else
  10. f(n)=f(n-1)+f(n-2);
  11. }
  12. int main()
  13. {
  14. int n=0;
  15. scanf("%d",&n);
  16. int ret=f(n);
  17. printf("%d",count);
  18. return 0;
  19. }

然而当我用迭代(循环)来实现的时候,就会快的多。

思维逻辑:1,1,2,3,5........

                   a    b

①斐波那契数列的第n项,即为前两项的和。
②当一共只有3项的时候,我们计算第三项的方法是把前两项加起来即得到第三项。
③而我们现在想要得到第n项,只用把a,b依次向后移动即可

那么我们怎么让a,b顺次移动呢?————中间量法

我们在a,b以外再定义一个变量,用于保存他们的和,作为新的b,再把b作为新的a即可。

即:

  1. int f(int n);
  2. int a=1;
  3. int b=1;
  4. int c=1;
  5. while(n>2)
  6. {
  7. c=a+b;
  8. a=b;
  9. b=c;
  10. n--;
  11. }
  12. return 0;
  13. return c;
  14. }

此时我们就只用计算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项的方法还有很多,如:矩阵快速幂等等,我们将会再后面的文章中提到。

创作不易,求各位看官,给我你们手中免费的点赞,收藏和关注吧❤

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/476486
推荐阅读
相关标签
  

闽ICP备14008679号