尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
阮一峰在《ECMAScript 6》中举了一个例子:
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
这是一个常规的Fibonacci 数列递归实现。但运行时需要保存众多调用帧,占用大量内存,容易发生栈溢出错误。
但若将其更改为尾递归:
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
则只存在一个调用帧,空间复杂度为O(1),因此永远不会发生栈溢出错误。
从例子中也可以看出尾递归的特征:最后return的只有递归函数,没有其他额外运算。
然而,ES6尾递归有一个极大的限制:只在严格模式下有效,正常模式无效。
常规模式下的优化
在常规模式下,依然是可以手动实现尾递归优化的。其思路为:使用循环来替换递归。
阮一峰举了一个例子:
var sum = function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1);
} else {
return x;
}
}
sum(1, 100000)
这是一个正常递归,也写成了尾递归的形式,便于进行手动优化。
现在实现一个通用的优化函数tco(),令其套用在尾递归函数sum()上后,可以实现优化。
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};
}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}
});
sum(1, 100000)
针对于该代码,阮一峰给出的解释为:
tco函数是尾递归优化的实现,它的奥妙就在于状态变量active。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。
若不理解优化的思路,那么该解释难以看懂。
该代码的流程图为:

其中:
- 调用
sum(),sum()对tco()返回的函数传入一个自定义的function()。tco()返回的是accumulator()。对于accumulator()而言,有3个公共的变量:value、active、accumulated。 - 将参数
arguments压入到accumulated中。此时accumulated为[[1, 10000]]。 - 第一次
active是false,故而可以进入if。然后将active置为true。这样做是为了阻止接下来进入递归。 - 进入
while,accumulated.shift()的作用是删除0号元素并返回该元素。故而此时拿到了刚刚保存的参数[1, 10000],并清空了accumulated。 - 执行
sum()内的函数。于是执行到了sum(x + 1, y - 1),参数变为[2, 9999],然后第二次进入了sum(),开始递归。 - 于是再次进入
accumulator(),此时公共变量accumulated大小为0。将[2, 9999]塞给它,大小变为1。 - 判断
if。由于在第3步中active置为true,故而此时if判断为false,无法进入。因此递归被中断。 - 但
if后没有任何逻辑了,因此执行完后返回的是undefined。所以此时返回到第5步value = f.apply(this, accumulated.shift());,因此该value的值是undefined。 - 再次进入
while判断。由于第6步将递归的参数塞给了accumulated,因此while判断为true,继续执行。此时再次从第4步开始,构成循环。 - 不断重复4-9步,直到
sum()内的if判定为false,即y == 0。此时就会返回x的最终运算结果。
综上,可以看出该优化截断了递归,并通过保存递归参数的方式,将递归运算转换为循环,从而避免了栈溢出。
1956

被折叠的 条评论
为什么被折叠?



