《编程珠玑(续)》中第13章习题8要求分析如下的递归转换迭代,即相当于把尾递归转换成循环。


function A(M)
{
<span >	</span>if(M==0)
		return X;
	else
	{
		S=A(M-1);
		return G(S,M);
	}
}

function B(M)
{
	S=X;
	for(j=1;j<=M;j++)
		S=G(S,j);
	return S;
}


G(S,M)就是典型的尾递归。至于G(S,M)+1是不是尾递归,找了些资料上说不是,但是可以很容易得将其转换成尾递归。比较好的参考资料:


尾递归优化


对于尾递归,以前的理解仅局限于它是递归和尾调用的终极合体,比普通递归效率高。至于效率为什么高,高在哪,一直没有深究过,现在补上。

 

要说尾递归,得先说尾调用。我理解的尾调用大概是这么一种情况:

函数A里面调用了函数B。

函数B执行后,函数A马上返回。

也就是说调用函数B(并返回执行结果)是函数A所做的最后一件事。

相当于执行完函数B后,函数A也就执行完。

因此在执行函数B时,函数A的栈帧其实是已经大部分没用了,可以被修改或覆盖。

编译器可以利用这一点进行优化,函数B执行后直接返回到函数A的调用者。

 

这里有一点需要注意:它是来自于编译器的优化。

 

这一点点的优化对于普通的尾调用来说可能意义不大,但是对于尾递归来说就很重要了。

 

尾递归是一种基于尾调用形式的递归,相当于前面说的函数B就是函数A本身。

 

普通递归会存在的一些问题是,每递归一层就会消耗一定的栈空间,递归过深还可能导致栈溢出,同时又是函数调用,免不了push来pop去的,消耗时间。

 

采用尾调用的形式来实现递归,即尾递归,理论上可以解决普通递归的存在的问题。因为下一层递归所用的栈帧可以与上一层有重叠(利用jmp来实现),局部变量可重复利用,不需要额外消耗栈空间,也没有push和pop。

 

再次提一下,它的实际效果是来自于编译器的优化(目前的理解)。在使用尾递归的情况下,编译器觉得合适就会将递归调用优化成循环。目前大多数编译器都是支持尾递归优化的。有一些语言甚至十分依赖于尾递归(尾调用),比如erlang或其他函数式语言(传说它们为了更好的处理continuation-passing style)。

 

假如不存在优化,大家真刀真枪进行函数调用,那尾递归是毫无优势可言的,甚至还有缺点——代码写起来不直观。

 

    现代编译器的优化能力很强悍,很多情况下编译器优化起来毫不手软(于是有了volatile)。但有时编译器又很傲娇,你需要主动给它一点信号,它才肯优化。尾递归就相当于传递一个信号给编译器,尽情优化我吧!

 

为了验证尾递归优化,可以写个小程序进行测试。在VS2010下将使用/O1或以上的优化选项,一般就会尾递归优化。Gcc3.2.2(这版本好旧)下一般需要使用-O2优化选项。

尾递归和普通递归的区别 java 尾递归和循环_尾递归和普通递归的区别 java

Sum是普通递归写法,sum_r采用了尾递归的写法。下面是它们的汇编代码,采用VS2010所带的cl.exe进行编译:cl.exe /Ox tail.c。

 

尾递归和普通递归的区别 java 尾递归和循环_递归_02

 

在00000011的位置可以看到sum中依然是call指令,而在0000002F的位置,sum_r直接使用jne实现循环跳转。

 

用Gcc3.2.2进行编译,结果类似:gcc tail.c –Os –o tail。

 

 

尾递归和普通递归的区别 java 尾递归和循环_编译器_03

其实我最初的写法是这样的:

 

尾递归和普通递归的区别 java 尾递归和循环_尾递归_04

在这种情况下,VS2010无法进行尾递归优化,或者说它没认出来,即使用了/0x。而gcc3.2.2依然是在开-O2选项的情况下就能认出尾递归,并进行优化。