代码改变世界

尾递归

2012-08-09 16:19  coodoing  阅读(460)  评论(1编辑  收藏  举报

      在传统递归方法中,每次重复的过程调用都使得调用链条不断加长. 系统不得不使用栈进行数据保存和恢复。如果单项链表十分长,那么上面这个方法就可能会遇到栈溢出,也就是抛出StackOverflowException。这是由于每个线程在执行代码时,都会分配一定尺寸的栈空间(Windows系统中为1M),每次方法调用时都会在栈里储存一定信息(如参数、局部变量、返回地址等等),这些信息再少也会占用一定空间,成千上万个此类空间累积起来,自然就超过线程的栈空间了。而尾递归就不存在这样的问题。

      尾递归相对传统递归,其是一种特例。在尾递归中,先执行某部分的计算,然后开始调用递归,所以你可以得到当前的计算结果,而这个结果也将作为参数传入下一 次递归。这也就是说函数调用出现在调用者函数的尾部,因为是尾部,所以方法之前所积累下的各种状态对于递归调用结果已经没有任何意义,因此完全可以把本次方法中留在堆栈中的数据完全清除,把空间让给最后的递归调用尾递归不是说不使用栈,而是在调用到自身的那句代码时候就可以释放临时变量占用的栈空间了,因为它们没有任何价值了,此时既不压入返回EIP,又消除了临时变量的栈,及时的返还了空间,所以栈上不会继续递增的使用空间,所以也不会产生栈溢出的现象这样的优化便使得递归不会在调用堆栈上产生堆积,意味着即时是“无限”递归也不会让堆栈溢出。这便是尾递归的优势。

      下面首先通过阶乘来说明递归和尾递归的区别,然后利用斐波那契数列的尾递归实现来说明。

第一部分、阶乘的尾递归

      1、普通递归实现

   1: long Rescuvie(long n) {      
   2:   return(n == 1) ? 1 : n * Rescuvie(n - 1);      
   3: }  

      当n = 5时,其递归过程如下:     

   1: Rescuvie(5)   
   2: {5 * Rescuvie(4)} 
   3: {5 * {4 * Rescuvie(3)}} 
   4: {5 * {4 * {3 * Rescuvie(2)}}} 
   5: {5 * {4 * {3 * {2 * Rescuvie(1)}}}} 
   6: {5 * {4 * {3 * {2 * 1}}}} 
   7: {5 * {4 * {3 * 2}}} 
   8: {5 * {4 * 6}} 
   9: {5 * 24} 
  10: 120

     2、尾递归实现

      尾递归的本质,其实是将递归方法中的需要的“所有状态”通过方法的参数传入下一次调用中。所以在阶乘计算的方法中,添加一个参数mul,它的功能是在递归调用时“积累”之前调用的结果,并将其传入下一次递归调用中。     

   1: long TailRescuvie(long n, long mul) {      
   2:   return(n == 1) ? mul : TailRescuvie(n - 1, mul * n);        
   3: }      

    对于n=5时,其递归过程如下:

   1: TailRescuvie(5, 1)
   2: TailRescuvie(4, 5)
   3: TailRescuvie(3, 20)
   4: TailRescuvie(2, 60)
   5: TailRescuvie(1, 120)
   6: 120

      容易看出, 普通的线性递归比尾递归更加消耗资源。而对于尾递归而言,由于保存了当前时刻的计算值,并将该值作为第二个参数传入下一个递归,使得系统不再需要保留之前计算结果,所以不会造成堆栈上的堆积和溢出。

第二部分、斐波拉契数列的尾递归

      1、普通递归实现

   1: public static int FibonacciRecursively(int n)
   2: {
   3:     if(n<2)
   4:         return n;
   5:     return FibonacciRecursively(n-1)+FibonacciRecursively(n-2);
   6: }
   2、尾递归实现

      改造成尾递归,我们则需要提供两个累加器:   

   1: // acc1:f(n-2) ; acc2:f(n-1)
   2: // f(n) = f(n-2)+f(n-1)
   3: public static int FibonacciTailRecursive(int n,int acc1,int acc2)
   4: {
   5:     if(n==0)
   6:         return acc1;
   7:     return FibonacciTailRecursive(n-1,acc2,acc1+acc2);
   8: }

      最后调用时候,需要提供两个累加器的初始值:FibonacciTailRecursive(6,0,1)。    

       最后在老赵的两篇关于尾递归的博文中,叙述了尾递归的优化方式以及Continuation在尾递归中的构造,分别利用IL汇编代码和lambda表达式进行说明。有兴趣的同学可以读一下。

1、尾递归与Continuation

2、浅谈尾递归的优化方式