探究递归内部的机制

      对于递归,接触过编程设计的人员都应该听说过。递归是一种常用的函数设计方法,记得国外的大神曾经说过一句话:是否真正理解指针和递归,与是否是一个优秀程序员直接相关。可见指针和递归在程序设计中的重要性。本文讨论的主要是对第二个递归的讨论,对于指针,其重要性不言而喻。可以这样说,对指针的理解程度的了解深度决定了你以后程序设计生涯的深度,因为不会指针,很多底层东西的源码你是一点都看不懂的。但是这不是本文讨论的重点。

      递归在大多数人的眼中,就是函数自己调用自己。但是有多少人了解了其内部的机制,函数为什么可以自己调用自己?递归的优势和劣势等等又在哪里?下面就根据我学到的东西来比较全面的了解一下递归。

      首先来看一个最常见也是最基本的例子来作为递归的入门,就是n的阶乘问题。一般求n的阶乘问题,常见有2种基本方法,一种就是迭代,一种就是递归。下面给出递归形式实现的代码:
      int digui(int n){
          if(n<1){
               return 1;
          }eles{
               return n*digui(n-1);
          }
    }
    这里采用了简单的方式来写这个递归。可以看到程序运行的else条件中,当判断n的值不小于1的时候,我们就用了n*digui(n-1)的方式,这个时候digui这个函数会继续调用自己,继续执行下去,当n的取值小于1的时候就返回1,这个时候递归结束。终止条件是递归的必须条件,否则函数会一直执行下去最终导致内存溢出现象。

     在上面的代码中可以看到,递归分为两个阶段,一是递推,二是回归。在递推阶段,函数通过调用自己来记录这一步的过程,当有终止条件产生的时候,递推就结束了。在回归阶段,以逆序的方式进行回归,当最后一个记录的程序也被回归后,整个递归就结束了,最后返回需要的东西。

     要了解递归的本质,就必须要对计算机的函数内部执行机制有一定的了解。在一个被定义的可执行函数中,是由4块区域组成的,分别是代码段,静态数据区,堆与栈。他们的分别作用是:代码段包含了这个函数的执行时的机器指令,在被转换成底层时,就是我们所说的01代码;静态数据区包含了当这个函数执行时的一些数据,如定义的变量的值等等;堆包含了动态内存的分配,在c语言中,就是由malloc函数申请的内存;栈包含了函数调用的一些信息。这里的栈就是我们理解递归最重要的一个信息。

     在程序设计语言中,当调用了一个函数的时候,栈中会分配一块空间来保存这个函数相关的信息。每一个被调用的空间都被看成一个活跃的记录,如果这个函数没有终止,那么函数调用产生的活跃记录一直会在栈中累积,直到这个函数调用结束。所以我们再来看一下,前面n的阶乘问题,当第一次调用digui(n)的时候,这个函数的活跃记录被存放在栈中,当执行else区域的时候因为返回n*digui(n-1),这个时候n-1被作为参数再次产生一个活跃记录放入栈中,因为这次的n-1是第一个活跃记录的返回参数,所以将会产生第二个活跃期参数,依次循环下去,直到n=1时。

     根据上面的讲解,看到栈其实就是存放函数调用信息的场所。因为栈先进后出的特点,可以保证函数的调用顺序。但是栈的缺点也很明显,每次在递归的调用过程中,每次都会开启一个新的活跃记录,这会造成栈的深度加深。栈的深度加深,意味着其占用的内存也会增大,而且在销毁调用空间的过程中,也会耗费一定的时间。如果仅仅是这样的递归,恐怕,递归在我们心目中的重要性也不会如此之大,一种叫做尾递归的技术很好的拟补了上面的不足。

     所谓尾递归,就是在函数调用的时候,不会产生新的栈空间,而是把原来的栈空间给覆盖了。它需要满足的条件就是,一个递归函数的返回值不属于表达式的一部分的时候,这样形成的递归就属于尾递归。我们先来看下前面n的阶乘实现的方法为什么不属于尾递归。因为在函数每次调用的时候,n*digui(n-1)的时候是依赖于前面的值n的,因此每次调用的函数都必须保存在栈中直到下一个函数被调用完成。我们做个简单的条件就可以把上面的递归函数改成尾递归函数。

 int digui(int n,int number){
  if(n<1){
   return 1;
  }else{
   return digui(n-1,number*n);
  } 
 }
 
    上面的代码就是简单的加一个参数来构造n的阶乘的尾递归。出来初始化的时候让number的初始值为1。每次递归调用的时候让number的值乘以当前的n,并把值作为第二个参数继续传递给下一次函数的调用。这个代码给前面实现的效果一样,但是它却是以尾递归的形式实现的。现在我们可以来明白下为什么这种写法就属于尾递归了。1.因为它是函数返回前的最后一条执行语句(后面可以还有其它的代码,只要这些代码在函数返回前没有执行也是可以的);2.它不依赖于前一函数的返回值,这样就不需要在栈中保留那个返回值参数。

    递归是一种很有用也很常用的东西,在树,图,排序等的数据结构中,我们可以看到递归大方面的应用,只有深入的了解了递归的一些本质东西,才能更好的掌握好递归。所以不要人云亦云,听到递归不好就放弃对递归的学习。
 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值