一、递归

简单来说,递归的思想就是:把问题分解为规模更小的、具有与原问题有着相同解法的问题。比如二分查找算法,就是不断地把问题的规模变小(变成原问题的一半),而新问题与原问题有着相同的解法。

一般来讲,能用递归来解决的问题必须满足两个条件:

  • 可以通过递归调用来缩小问题规模,且新问题与原问题有着相同的形式。
  • 存在一种基准情形(即出口),可以使递归在基准情形下退出。

以计算n!为例,递归的算法为:

publicstaticlong factorial(long n){

        if(n==1){

            return 1;

        }

        return n*factorial(n-1);

    }

 

有些问题使用传统的迭代算法是很难求解甚至无解的,而使用递归却可以很容易地解决,比如汉诺塔问题。递归使得程序结构清晰易懂,但递归也有它的劣势。因为递归调用实际上是函数不断调用自身,每调用一次函数,系统都将活动记录(activation record)或叫做栈帧(frame stack)(存储函数当前的变量、返回地址等信息)压入到栈中。当调用多次时,不断的压栈,很容易导致栈的溢出。而在函数调用结束后,还要释放空间,弹栈恢复断点。这样,不仅浪费空间,还浪费时间。

所以,程序员们经常用递归实现最初的版本,然后对它进行优化,改写为循环以提高性能。尾递归于是进入了人们的眼帘。

二、尾递归

当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,递归结果可以直接返回,这个特性很重要,因为大多数现代的编译器会利用这个特点自动优化代码(通过循环的方法)。在编译器发现在做尾递归的时候,就不会去不断创建新的栈帧,而是不断的去覆盖当前的栈帧,一来防止栈溢出,二来节省了时间,用《算法精解》里面的原话就是:"When a compiler detects a call that is tail recursive, it overwrites the current activation record instead of pushing a new one onto the stack."。注意,java,C#和python都不支持编译环境自动优化尾递归。

将以上计算n!的普通递归算法改写成尾递归方式为:

publicstaticlong factorial(long n,long a){

        if(n==1){

            return a;

        }

        returnfactorial(n-1, n*a);

    }

 

函数中的参数a,它是一个累加器(accumulator,习惯上翻译为累加器,其实不一定非是"加",任何形式的积聚都可以),用来积累之前调用的结果,这样之前调用的数据就可以被丢弃了。

三、普通递归改写为尾递归

将普通递归改写为尾递归,关键在于找到合适的累加器。我们以计算斐波拉契序列(fibonacci)为例,演示如何将普通递归改写为尾递归。斐波拉契序列的前两项为1,从第三项开始,每项都是前面两项的和。下面是按照其定义给出的递归算法:

publicstaticlong fibonacci(long n){

        if(n<=2){

            return 1;

        }

        returnfibonacci(n-1)+fib(n-2);

    }

 

该算法的复杂度为O(2N),因为在算fib(n-1)时又计算了一次fib(n-2),违反了合成效益法则,即"计算任何事情不要超过一次"。

我们发现,计算每项时,需要知道前面两项,所以我们这里要有两个累加器,使用累加器的尾递归算法为:

publicstaticlong fibonacci(long n,long a,long b){

        if(n==1){

            return a;

        }

        returnfibonacci(n-1, b, a+b);

    }

 

以n=5为例,a=1,b=1分别表示序列的第一、二项:

fibonacci(5,1,1)

->fibonacci(4,1,2)

->fibonacci(3,2,3)

->fibonacci(2,3,5)

->fibonacci(1,5,8)

此时a=5,b=8,结果为a的值5。整个过程为从前往后累积的过程,而计算n!是从后往前累积。

将尾递归改写成非递归的循环方式很容易,如下:

publicstaticlong fibonacci(long n){

        int a=1,b=1;

        int temp=0;

        while(n>1){

            temp=b;

            b=a+b;

            a=temp;

            n--;

        }

        return a;

    }

 

四、总结

由于Java编译器并不支持自动优化尾递归,如果问题用迭代的方法可解,我们最好不要让程序带着尾递归。