尾递归(Tail recursion / Tail call)

递归与尾递归

关于递归操作,简单地说,一个函数直接或间接地调用自身,是为直接或间接递归。例如,可以使用递归来计算一个单向链表的长度:

public static int GetLengthRecursively(Node head)
{
    if (head == null) return 0;
    return GetLengthRecursively(head.Next) + 1;
}

在调用时,GetLengthRecursively方法会不断调用自身,直至满足递归出口。对递归有些了解的朋友一定猜得到,如果单向链表十分长,那么上面这个方法就可能会遇到栈溢出,也就是抛出StackOverflowException。这是由于每个线程在执行代码时,都会分配一定尺寸的栈空间(Windows系统中为1M),每次方法调用时都会在栈里储存一定信息(如参数、局部变量、返回地址等等),这些信息再少也会占用一定空间,成千上万个此类空间累积起来,自然就超过线程的栈空间了。不过这个问题并非无解,我们只需把递归改成如下形式即可(这篇文章里不考虑非递归的解法):

public static int GetLengthTailRecursively(Node head, int acc)
{
    if (head == null) return acc;
    return GetLengthTailRecursively(head.Next, acc + 1);
}

GetLengthTailRecursively方法多了一个acc参数,acc的为accumulator(累加器)的缩写,它的功能是在递归调用时“积累”之前调用的结果,并将其传入下一次递归调用中——这就是GetLengthTailRecursively方法与GetLengthRecursively方法相比在递归方式上最大的区别:GetLengthRecursive方法在递归调用后还需要进行一次“+1”,而GetLengthTailRecursively的递归调用属于方法的最后一个操作。这就是所谓的“尾递归

与普通递归相比,由于尾递归的调用处于方法的最后,因此方法之前所积累下的各种状态对于递归调用结果已经没有任何意义,因此完全可以把本次方法中留在堆栈中的数据完全清除,把空间让给最后的递归调用。这样的(编译器)优化便使得递归不会在调用堆栈上产生堆积,意味着即使是“无限”递归也不会让堆栈溢出。这便是尾递归的优势。

有些朋友可能已经想到了,尾递归的本质,其实是将递归方法中的需要的“所有状态”通过方法的参数传入下一次调用中。对于GetLengthTailRecursively方法,我们在调用时需要给出acc参数的初始值:

GetLengthTailRecursively(head, 0);

为了进一步熟悉尾递归的使用方式,我们再用著名的“菲波纳锲”数列作为一个例子。传统的递归方式如下:

public static int FibonacciRecursively(int n)
{
    if (n < 2) return n;
    return FibonacciRecursively(n - 1) + FibonacciRecursively(n - 2);
}

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

public static int FibonacciTailRecursively(int n, int acc1, int acc2)
{
    if (n == 0) return acc1;
    return FibonacciTailRecursively(n - 1, acc2, acc1 + acc2);
}

于是在调用时,需要提供两个累加器的初始值:

FibonacciTailRecursively(10, 0, 1);

尾递归的循环优化

尾递归,即是递归调用放在方法末尾的递归方式,如经典的阶乘:

int FactorialTailRecursion(int n, int acc)
{
    if (n == 0) return acc;
    return FactorialTailRecursion(n - 1, acc * n);
}

由于递归在方法的末尾,因此方法中的局部变量已经毫无用处,编译器完全可以将其“复用”,并把尾递归优化为“循环”方式:

int FactorialLoopOptimized(int n, int acc)
{
    while (true)
    {
        if (n == 0) return acc;

        acc *= n;
        n--;
    }
}

c编译器(gcc)对尾递归的优化

在gcc编译的时候加上-O2会对尾递归进行优化。我们可以直接看生成的汇编代码。(使用gdb, gcc –O2 factorial.c –o factorial;    disass factorial)

未加-O2生成的汇编:

加了O2优化的汇编:

去网上稍微搜搜汇编命令,大致就能理解如下:

function factoral(n, sum) 
{
    while(n != 0)
    {
        sum = n * sum
        n = n-1
    }
    return sum
}

gcc对尾递归进行了智能优化。如果还有兴趣,可以使用-O3对尾递归进行优化,并查看其中的汇编指令。-O3的优化是直接将循环展开。

一些其他网友对尾递归的讨论

尾递归就是从最后开始计算,每递归一次就算出相应的结果。而线形递归是直到递归到一个确定的值后,又从这个具体值向后计算。所以线形递归肯定费事。

本文内容截取自以下三篇文章,感谢原文作者,让我对尾递归有了更好的理解。-_-

尾递归与Continuation:http://www.cnblogs.com/JeffreyZhao/archive/2009/03/26/tail-recursion-and-continuation.html

浅谈尾递归的优化方式:

http://www.cnblogs.com/JeffreyZhao/archive/2009/04/01/1424028.html

又见尾递归:

http://www.cnblogs.com/yjf512/archive/2012/07/12/2588481.html

posted on 2013-01-30 17:04  zhuyf87  阅读(3343)  评论(1编辑  收藏  举报

导航