赞
踩
什么是尾递归呢?(tail recursion), 顾名思议,就是一种“不一样的”递归,说到它的不一样,就得先说说一般的递归。对于一般的递归,比如下面的求阶乘,教科书上会告诉我们,如果这个函数调用的深度太深,很容易会有爆栈的危险。
// 先不考虑溢出问题
int func(int n)
{
if (n <= 1) return 1;
return n * func(n-1);
}
原因很多人的都知道,让我们先回顾一下函数调用的大概过程:
因此,在函数 A 执行的时候,如果在第二步中,它又调用了另一个函数 B,B 又调用 C… 栈就会不断地增长不断地装入数据,当这个调用链很深的时候,栈很容易就满了,这就是一般递归函数所容易面临的大问题。
而尾递归在某些语言的实现上,能避免上述所说的问题,注意是某些语言上,尾递归本身并不能消除函数调用栈过长的问题,那什么是尾递归呢?在上面写的一般递归函数 func() 中,我们可以看到,func(n) 是依赖于 func(n-1) 的,func(n) 只有在得到 func(n-1) 的结果之后,才能计算它自己的返回值,因此理论上,在 func(n-1) 返回之前,func(n),不能结束返回。因此func(n)就必须保留它在栈上的数据,直到func(n-1)先返回,而尾递归的实现则可以在编译器的帮助下,消除这个限制:
// 先不考虑溢出
int tail_func(int n, int res)
{
if (n <= 1) return res;
return tail_func(n - 1, n * res);
}
从上可以看到尾递归把返回结果放到了调用的参数里。这个细小的变化导致,tail_func(n, res)不必像以前一样,非要等到拿到了tail_func(n-1, nres)的返回值,才能计算它自己的返回结果 – 它完全就等于tail_func(n-1, nres)的返回值。因此理论上:tail_func(n)在调用tail_func(n-1)前,完全就可以先销毁自己放在栈上的东西。
这就是为什么尾递归如果在得到编译器的帮助下,是完全可以避免爆栈的原因:每一个函数在调用下一个函数之前,都能做到先把当前自己占用的栈给先释放了,尾递归的调用链上可以做到只有一个函数在使用栈,因此可以无限地调用!
附上斐波那契数列的尾递归实现
前面的讨论一直都集中在尾递归上,这其实有些狭隘,尾递归的优化属于尾调用优化这个大范畴,所谓尾调用,形式它与尾递归很像,都是一个函数内最后一个动作是调用下一个函数,不同的只是调用的是谁,显然尾递归只是尾调用的一个特例。
int func1(int a)
{
static int b = 3;
return a + b;
}
int func2(int c)
{
static int b = 2;
return func1(c+b);
}
上面例子中,func2在调用func1之前显然也是可以完全丢掉自己占有的栈空间的,原因与尾递归一样,因此理论上也是可以进行优化的,而事实上这种优化也一直是程序编译优化里的一个常见选项,甚至很多的语言在标准里就直接要求要对尾调用进行优化,原因很明显,尾调用在程序里是经常出现的,优化它不仅能减少栈空间使用,通常也能给程序运行效率带来比较大的提升。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。