什么是尾递归?就是函数返回之前的最后一个操作若是递归调用,则该函数进行了尾递归。先看一个例子:
例子1:阶乘
//阶乘递归实现
int FactorialRecursion(int n)
{
if(n==0) retrun 1;
else retrun n*FactorialRecursion(n-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--;
}
}
阶乘的递归实现,函数返回之前的最后一个操作为乘法,不是递归调用,所以不是尾递归。
尾递归与循环:
我们可以发现,其实尾递归的过程和循环基本上是等价的,我们可以将尾递归的过程很方便到用循环来代替,所以很多的语言对尾递归提供了编译级别的优化,也就是将尾递归在编译期转化成循环的代码。不过对于没有提供尾递归优化的语言来说也是很有意义的,
由于递归在方法的末尾,因此方法中的局部变量已经毫无用处,编译器完全可以将其“复用”,并把尾递归优化为“循环”方式。所以尾递归的方式,类似循环。也可以说,尾递归可以转换为循环来实现。据网上查阅,java,C#和python都不支持编译环境自动优化尾递归,但是对于C语言来说,编译器白提供的服务,用了也不差,毕竟递归代码会好理解一点,但换句话说,如果写到尾递归这份上了,变成非递归已经很好实现了,完全可以用循环来搞定,所以呢,这个时候,就看个人喜好了。
例子2:斐波那契问题
//递归实现
int fabonacci(int n)//复杂度为指数级
{
if(n==0) return 0;
else if(n==1) return 1;
else
{
return fabonacci(n-1)+fabonacci(n-2);
}
}
//循环实现
int fabonacci2(int n)//复杂度:O(n)
{
int fab;
int a=1,b=1;
if(n==0) return 0;
else if(n==1) return 1;
else
{
for(int i=2;i<=n;i++)
{
fab=a+b;
a=b;
b=fab;
}
}
return fab;
}
//斐波那契尾递归实现
int fabtrail(int n,int a,int b)//复杂度:O(n)
{
if(n==0) return a;
else if(n==1) return b;
else return fabtrail(n-1,b,a+b);
}
编译器是怎样优化尾递归的?
我们知道递归调用是通过栈来实现的,每调用一次函数,系统都将函数当前的变量、返回地址等信息保存为一个栈帧压入到栈中,那么一旦要处理的运算很大或者数据很多,有可能会导致很多函数调用或者很大的栈帧,这样不断的压栈,很容易导致栈的溢出。
我们回过头看一下尾递归的特性,函数在递归调用之前已经把所有的计算任务已经完毕了,他只要把得到的结果全交给子函数就可以了,无需保存什么,子函数其实可以不需要再去创建一个栈帧,直接把就着当前栈帧,把原先的数据覆盖即可。相对的,如果是普通的递归,函数在递归调用之前并没有完成全部计算,还需要调用递归函数完成后才能完成运算任务,比如return n * FactorialRecursion(n - 1);这句话,这个FactorialRecursion(n)在算完FactorialRecursion(n-1)之后才能得到n * FactorialRecursion n - 1)的运算结果然后才能返回。
综上所述,编译器对尾递归的优化实际上就是当他发现你在做尾递归的时候,就不会去不断创建新的栈帧,而是就着当前的栈帧不断的去覆盖,一来防止栈溢出,二来节省了调用函数时创建栈帧的开销,用《算法精解》里面的原话就是:“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实现了斐波那契的尾递归,虽然算法复杂度从指数级降到了线性增长,但当n=100000时,就出现了stackOverFlowError,而在c中不会出现,说明java是没有对尾递归进行优化的,仍会出现堆栈溢出。
这个怎么说呢,据网上查阅,java,C#和python都不支持编译环境自动优化尾递归,这种情况下,当然是别用递归效率最高,可以看下这里http://www.cnblogs.com/Alexander-Lee/archive/2010/09/16/1827587.html(利用抛出异常,自己捕获,优化尾递归)。
注:http://www.cnblogs.com/JeffreyZhao/archive/2009/04/01/tail-recursion-explanation.html(老赵-尾递归优化)
这里提出了Continuation技术,不懂