大家都比较熟悉递归,但递归比较占资源,而且容易溢出。
相比之下,下面我要引入的尾递归会比较高效。
尾递归就是从最后开始计算, 每递归一次就算出相应的结果, 也就是说, 函数调用出现在调用者函数的尾部, 因为是尾部, 所以根本没有必要去保存任何局部变量. 直接让被调用的函数返回时越过调用者, 返回到调用者的调用者去.
说白了, 一个对自己本身的递归 尾调用,就叫做尾递归。这里尾调用的“尾”字,是指运行
时需要执行的最后一个动作。不是简单的语法字面上的最后一个语句。 尾递归实
际执行的是迭代的计算过程。
线性递归函数必须满足以下两个基本属性:
*必须清晰无误地解决基的情况。
*每一个递归的调用,必须包含更小的参数值。
而尾递归则不必满足这两个条件。
普通的线性递归比尾递归更加消耗资源, 在实现上说, 每次重复的过程调用都使得调
用链条不断加长. 系统不得不使用栈进行数据保存和恢复.而尾递归就不存在这样的问
题, 因为它的状态完全由函数的参数保存. 并且,由于尾递归的函数调用出现在调用者
函数的尾部,因为是尾部,所以根本没有必要去保存任何局部变量的信息。
直接让被调用的函数返回时越过调用者,返回到调用者的调用者去。
尾调用优化不是什么很复杂的优化,实际上几乎所有的现代的高级语言编译器都支持
尾调用这个很基本的优化。 实现层面上,只需要把汇编代码call改成jmp, 并放弃所有
局部变量压栈处理,就可以了。这样一来,堆栈根本就没有被占用,每次调用都是重
新使用调用者的堆栈。
尽管尾递归比递归高效,但并非所有的递归算法都可以转成尾递归的,因为尾递归本质
上执行的是迭代的计算过程。这与并非所有的递归算法都可以转成迭代算法的原因是一样的。
以下是具体实例:
线性递归:
long Rescuvie(long n) {
return(n == 1) ? 1 : n * Rescuvie(n - 1);
}
尾递归:
long TailRescuvie(long n, long a) {
return(n == 1) ? a : TailRescuvie(n - 1, a * n);
}
long TailRescuvie(long n) {//封装用的
return(n == 0) ? 1 : TailRescuvie(n, 1);
}
当n = 5时
对于线性递归, 他的递归过程如下:
Rescuvie(5)
{5 * Rescuvie(4)}
{5 * {4 * Rescuvie(3)}}
{5 * {4 * {3 * Rescuvie(2)}}}
{5 * {4 * {3 * {2 * Rescuvie(1)}}}}
{5 * {4 * {3 * {2 * 1}}}}
{5 * {4 * {3 * 2}}}
{5 * {4 * 6}}
{5 * 24}
120
对于尾递归, 他的递归过程如下:
TailRescuvie(5)
TailRescuvie(5, 1)
TailRescuvie(4, 5)
TailRescuvie(3, 20)
TailRescuvie(2, 60)
TailRescuvie(1, 120)
120
很容易看出, 普通的线性递归比尾递归更加消耗资源, 在实现上说, 每次重复的过程
调用都使得调用链条不断加长. 系统不得不使用栈进行数据保存和恢复.而尾递归就
不存在这样的问题, 因为他的状态完全由n和a保存.
(以上实例 摘自百度百科)
下面再看一个例子(java代码): Fibonacci数列的尾递归实现。
(摘自http://www.soidc.net/articles/1215485053486/20080906/1215945561634_1.html)
使用递归方式解决问题是一种比较直观而且简洁的方式,不过编译器对递归没有特别的优化.所以我们很容易写出效率不高的递归程序.而所谓尾递归就是在递归的时候进行计算.下面我以Fibonacci数列为例来说明普通递归和尾递归的不同.
普通递归:
public long Fib_Common(int n)
{
if (n == 1 || n == 2)
return 1;
else
return Fib_Common(n - 1) + Fib_Common(n - 2);
}
这个实现简单明了就是执行速度太慢了,因为编译器是以如下方式进行计算的(例如计算Fib(6)):
Fib(6) = Fib(5) + Fib(4);
= Fib(4) + Fib(3) + Fib(3) + Fib(2);
= Fib(3) + Fib(2) + Fib(2) + Fib(1) + Fib(2) + Fib(1) + Fib(2);
= Fib(2) + Fib(1) + Fib(2) + Fib(2) + Fib(1) + Fib(2) + Fib(1) + Fib(2);
= 8
从上面的递归展开式可以看出Fib(4),Fib(3)都被计算了2次,而且递归函数以2的指数增长。所以当计算到30时就变得非常慢。
尾递归:
public long Fib_Tail(int n)
{
if (n == 1 || n == 2)
return 1;
else
return Tail(n, 1, 1, 3);
}
private long Tail(int n, long b1, long b2, int begin)
{
if (n == begin)
{
return b1 + b2;
}
else
return Tail(n, b2, b1 + b2, begin + 1);
}
再来看看Fib_Tail(3)的计算过程
Fib_tail(6) = Tail(6, 1, 2, 3)
= Tail(6, 2, 3, 4)
= Tail(6, 3, 5, 5)
= 8
这样计算过程都是在每次进入递归函数时计算的(尾部),所以是一个线性增长。只要编译器允许我们可以计算Fib_tail(100)都非常迅速.
正常循环:
public long Fib_Loop(int n)
{
long b1 = 1, b2 = 1, temp, begin = 3;
for (; begin <= n; ++begin)
{
temp = b1;
b1 = b2;
b2 = temp + b2;
}
return b2;
}