尾调用优化存在于许多语言和编译器中。在这种情况下,编译器会识别以下形式的函数:
int foo(n) {
...
return bar(n);
}
在这里,该语言能够识别出返回的结果是来自另一个函数的结果,并将具有新堆栈帧的函数调用更改为跳转。
实现经典析因方法:
int factorial(n) {
if(n == 0) return 1;
if(n == 1) return 1;
return n * factorial(n - 1);
}
是不是因为回报必要的检查尾调用optimizatable。(示例源代码和编译输出)
为了使该尾调用可优化,
int _fact(int n, int acc) {
if(n == 1) return acc;
return _fact(n - 1, acc * n);
}
int factorial(int n) {
if(n == 0) return 1;
return _fact(n, 1);
}
使用编译此代码gcc -O2 -S fact.c
(-O2是启用编译器中的优化所必需的,但是-O3的更多优化会使人难以阅读...)
_fact(int, int):
cmpl $1, %edi
movl %esi, %eax
je .L2
.L3:
imull %edi, %eax
subl $1, %edi
cmpl $1, %edi
jne .L3
.L2:
rep ret
(示例源代码和编译输出)
可以在segment中看到.L3
,jne
而不是a call
(它使用新的堆栈框架进行子例程调用)。
请注意,这是使用C完成的。Java中的尾调用优化非常困难,并且取决于JVM的实现(也就是说,我还没有看到任何实现该功能的方法,因为这很困难,并且需要堆栈框架的Java安全模型也有其含义。 -TCO避免了这种情况)-tail-recursion + java和tail-recursion +优化是浏览的好标记集。您可能会发现其他JVM语言可以优化尾递归更好(TRY的Clojure(这需要RECUR到尾调用优化),或斯卡拉)。
那就是
知道您写的东西是对的 -这是一种可以实现的理想方式,这一定使您感到高兴。
现在,我要弄些苏格兰威士忌,戴上一些德国电子琴 ...
对于“在递归算法中避免堆栈溢出的方法”这一普遍问题,...
另一种方法是包括递归计数器。这更多地用于检测由无法控制的情况(以及不良的编码)导致的无限循环。
递归计数器的形式为
int foo(arg, counter) {
if(counter > RECURSION_MAX) { return -1; }
...
return foo(arg, counter + 1);
}
每次拨打电话时,您都会增加计数器。如果计数器太大,则会出错(在这里,仅返回-1,但是在其他语言中,您可能更喜欢抛出异常)。这样做的目的是防止进行比预期深得多的循环(可能是无限循环)时,发生更糟的事情(内存不足错误)。
从理论上讲,您不需要此。在实践中,由于大量的小错误和不良的编码实践(多线程并发问题,其中方法外的某些更改使另一个线程进入了递归调用的无限循环),我见过写得不好的代码。
使用正确的算法并解决正确的问题。专门针对Collatz猜想,您似乎正在尝试以xkcd的方式解决它:
您从一个数字开始并且正在遍历树。这迅速导致非常大的搜索空间。快速运行以计算出正确答案的迭代次数大约需要500步。对于具有小堆栈框架的递归来说,这不应该成为问题。
虽然知道递归解决方案不是一件坏事,但人们还应该认识到,迭代解决方案通常会更好。从递归到迭代的方式在堆栈溢出中可以看到多种将递归算法转换为迭代算法的方法。