非尾递归,下一个函数结束以后此函数还有后续,所以必须保存本身的环境以供处理返回值。
尾递归,进入下一个函数不再需要上一个函数的环境了,得出结果以后直接返回。
递归(迭代):
recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
5 + (4 + (3 + 3))
5 + (4 + 6)
5 + 10
15
tailrecsum(5, 0)
tailrecsum(4, 5)
tailrecsum(3, 9)
tailrecsum(2, 12)
tailrecsum(1, 14)
tailrecsum(0, 15)
15
尾递归的判断标准是函数运行最后一步是否调用自身,而不是是否在函数的最后一行调用自身。
这是尾递归:
function f(x) {
if (x === 1) return 1;
return f(x-1);
}
这不是尾递归:
function f(x) {
if (x === 1) return 1;
return 1 + f(x-1);
}
通常递归都是在栈上根据调用顺序依次申请空间进行运算,然后层层回调,这是基于上一层运算依赖于下一层的运算结果(或者说上一层的运算还没用做完,需要下一层返回的结果)
而尾递归的情况是下层计算结果对上层“无用”(上一层运算已经做完,不依赖后续的递归),为了效率,直接将下一层需要的空间覆盖在上一层上。
所以
尾递归,比线性递归多一个参数,这个参数是上一次调用函数得到的结果;
所以,关键点在于,尾递归每次调用都在收集结果,避免了线性递归不收集结果只能依次展开消耗内存的坏处。
使用尾递归可以带来一个好处:因为进入最后一步后不再需要参考外层函数(caller)的信息,因此没必要保存外层函数的stack,递归需要用的stack只有目前这层函数的,因此避免了栈溢出风险。