寥寥数语讲明白函数递归

什么是函数递归

递归是C语言中解决问题的一种方法。故明思议,递,递推;归,回归。函数递归在形式表现为函数自己调用自己。

函数递归的思想

函数递归是一种方法,那么将高度概括它的思想是什么呢?将一个规模较大较复杂的大问题,拆分为数个规模较小的相似的小问题(子问题)来求解,直到小问题(子问题)不能被拆分,递归结束。

函数递归的书写

函数递归有一个限制条件,一旦达到这限制条件,递归便不再继续。

递归程序每运行一次,就会越靠近限制条件。

函数递归的实践前言

作为解决问题的一种方法,函数递归很难用概念讲清,接下来会用函数递归的方法来解决几个典型的问题,读者跟着问题去理解,相信很快就会领会函数递归的精髓。

用递归法求阶层

什么是阶层?例如5的阶层5!=5*4*3*2*1=120,这便是阶层。

如何用代码表示出来

#include<stdio.h>

int main()

{
    int n=1;
    int ret=1;

  for(n=1;n<=5;n++)
   
   {
      ret*=n;
   }
   
   return 0;
}

这就是普通的求法。

如何用递归法求阶层呢?

来看一断推导

5!=5*4*3*2*1

4!=4*3*2*1

那么5!=5*4!

现在我写一个函数fact()用来求阶层,那么n!=n*fact(n-1)。有了思路,便可以用递归的方法写代码了

我要做的就是把5的阶层拆分5*4!,把4!阶层拆分为4*3!,这样层层往下,直至不可拆为止。

这样我们求阶层的代码(函数)便写好了。这就是我说的递归表现为函数自己调用自己,正如图中函数fact自己调用自己一样。 

int fact(int n)

{
   if(n==0)
     return 1;

   else
     return n*fact(n-1);
}

这是完整的代码。

为什么函数自己调用自己叫递归呢,下面我就详细讲解过程,给读者领会一下。


这就是程序运行的过程,类似复合函数,在没达到限制条件0之前,不断调用自己

函数拆分,直至达到限制条件。开始不断返回计算值给上一层函数,最终完成运算。

过程如下:我要求5的阶层,把5传如fact(n)(得fact(5))函数中,5不等于零,return n*fact(n-1)即5*fact(4)中。调用第二层函数fact(n),此时第二层函数n等于4

fact(4),4不等于零,returnn*fact(n-1)即4*fact(3),调用第三层函数fact(3)。

同理3*fact(2),依次调用fact(2)。fact(2)内部即2*fact(1)

fact(1)时,n=0,返回1给函数fact(1),那么2*fact(1)=2,返回给fact(2),那么3*fact(2)=6,返回给fact(3),那么4*fact(3)=24,返回fact(5)即最终5*24=120。

  

顺序打印整数的每一位

例题  输入520;输出 5 2 0

         输入1234;输出1 2 3 4

思路

以1234为例,想要得到它的每一位,我们发现1234除以10,余数4;而123除以10,余数是3;12除以10,余数是2;最后是1,模上10即1%10(模%,求余数),我们的计算机给出的结果是1。

这样思路就来了,我们可以通过%10求余数来得到每一位数,通过除以十来减少1234的位数,再%10。这样就可看到1234%10=4,1234/10=123;123%10=3,123/10=12;12%10=2,12/10=1:最后1%10=1。,那么我需要写一个用来打印它们每一位的函数就叫print()吧。

你也许会问依次模10最终打印的不是4 3 2 1吗,其实并不是依次模10,按我理解恰恰是递归归的部分依次由里及外依次打印1 2 3 4的。就有点类似我们上一个例子依次向外(或向上)返回2,6,24一样。上代码

int print(int n)

 {
    if(n>9)
    {
       print(n/10);//开始自己调用自己,依次为print(1234),print(123),print(12),print(1).
    }

    printf("%d "n%10);//%d后面有个空格
 }

完整代码。

下面说说详细过程

print()的执行顺序先把if语句(if语句里调用print()) “递推”完,得n%10=1,开始执行每个print()函数里头的打印printf函数,先打印1;

再执行外部或者说上一个print()里的打印printf函数,上一个是print(12),那么则是打印12%10得2;

接着往上是print(123),那么则是打印123%10=3;

最后往上print(1234),那么则是打印1234%10=4;

类似复合函数层层的套。

求第n项斐波那契数列

还有个更极端的例子,用递归求斐波那契数列。如果是做题可用递归(管它黑猫白猫),但在实际工作中,是不适合用递归的,为什么?然后我将由这个例子引出递归的缺点,更加深刻地讲解递归。

什么是斐波那契数列

来看一组数

1,1,2,3,5,8,13,21.......;

这组数你发现什么规律吗?没错,第n项等于前两项的和,例如1+1=2;1+2=3;2+3=5;3+5=8......。而这正是斐波那契数列。

递归求解

有了思路,现在我要创造一个fib()函数,用来求斐波那契数列的第n项,那么公式就出来了,顺便上代码

            

int fib(n)
  
{
   if(n<2)
      return 1;
  
    else
      return fib(n-1)+fib(n-2);

}

这个代码的运行过程很复杂,所以我就不一一画了,但可以画思路图,思路并不复杂。计算机也是这样感觉的,我调试时输入较大的值时例如50或100时,风扇呼呼地响了,然后cpu嘎嘎涨,几乎运行cpu的一半,以下是部分过程截图。最后还是没有算出来,我也不想等,还有一次任务中断了。我的电脑配置中等,游戏本不算差。

可以看出递归效率比较低,尤其是这种极端的情况下。为什么?

这是过程图,我发现再写也写不下,就不写了。由上图,你会发现每调用一次fib()内部就调用两次,如此递增层层套娃,还会重复计算,大大降低了效率。然后这对内存也是一种负担,数次多的递归还容易导致栈溢出。

递归的缺点及原理

通过对斐波那契的例子,我们明白了,递归存在缺点,多次的函数调用,可能会重复计算。当然最后我提了一下对内存的负担和栈溢出。下面我就讲一下这些缺点的原理。

函数调用的原理:函数调用会专门为函数在内存中开辟一快空间,函数运算结束后,运算值会返回给函数而开辟的空间会被销毁,如果函数调用迟迟不结束,并且还在里面嵌套调用函数,一层层地调。那么内存会一直被开辟和占用,直到函数运算结束,开始返回值。由此可知函数递归:

  1. 内存消耗大:每次函数调用都需要在栈中分配内存,递归调用次数过多时会导致栈溢出。
  2. 运行速度慢:递归调用的过程中需要频繁的压栈和出栈操作,影响程序的运行速度。
  3. 可读性差:递归函数的代码结构比较复杂,容易让人难以理解和维护。
  4. 存在潜在的死循环风险:如果递归函数没有正确的终止(限制)条件,就会出现死循环,导致程序无法正常运行。

具体的递归原理的底层涉及函数栈帧,这类知识有机会也会整理成博客。

补充一下,为了更直观的体现出递归的缺点,我定义了了一个count,程序每运行一次count加一,算50的阶层总共运行了512559680次,运行时间为两分多钟,然后结果还是负值,为什么?栈溢出了即为阶层函数开辟的栈空间不足,溢出并开始重新计算,所以为负值。

以上是作者学习时的浅薄总结,有误请见谅,若指出或讨论,不甚感激。

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清宁长欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值