对尾递归的理解
首先要理解递归和尾调用
递归
递归:一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,每次调用的方法主体一样,参数不一样,必须定义条件使递归能够停下。分为直接递归和间接递归,直接递归:方法自身调用自己,间接递归:A方法调用B方法,B方法调用C方法,C方法调用A。
下面是一个求阶乘的列子,最终得到的结果是5*4*3*2*1
function getSum(n){
if(n==1)return 1
return n * getSum(n-1)
}
getSum(5)尾调用:
尾调用:指函数的最后调用另一个函数。尾调用不一定出现在函数尾部,只要是最后一步操作即可。必须在最后返回另一个函数的调用结果才算尾调用。不属于尾调用:return 1 + a(10)属于尾调用:return a(1 + 10)
例子:
function a(args){
return b(args)
}
function b(args){
return args + 1
}那么尾调用解决了什么问题?
个人的理解就是 只保存一次调用记录,防止栈溢出。
函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。
尾递归
尾递归:函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
回到递归的第一个例子,看看函数调用过程。
第一次调用 返回 5 * getSum(5-1)
第二次调用 返回 4 * getSum(4-1)
第三次调用 返回 3 * getSum(3-1)
第四次调用 返回 2 * getSum(2-1)
第五次调用 返回 1
可以看到,每次调用都需要保留参数n,等待下一次调用返回结果时,才能计算出本次调用的结果。
这样形成了一个依赖关系,第一次调用的结果需要等后续调用结束后才能计算出最终的结果。
如果调用几千次,就需要保存几千次调用记录,很容易发生"栈溢出"错误(stack overflow),但对于尾递归来说,由于只存在一个调用记录,所以不会发生栈溢出。
结合尾调用将第一个例子进行优化:
function getSum(sum,n){
if(n==1)return sum
return getSum(sum*n,n-1)
}结论:尾递归能防止出现栈溢出,减少内存占用。
JS中的尾调用
ES6的尾调用优化只在严格模式下开启,正常模式是无效的。 这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。 arguments:返回调用时函数的参数。func.caller:返回调用当前函数的那个函数。 尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
参考文章:http://www.ruanyifeng.com/blog/2015/04/tail-call.html
评论
后参与评论
还没有评论,来说点什么吧~