尾调用
指某个函数的最后一步是调用另一个函数
1:
function f(x){
return g(x);
}
2:
function f(x){
let y = g(x);
return y;
}
3:
function f(x){
return g(x) + 1;
}
1是尾调用,2/3不是
注意:尾调用不一定出现在函数尾部,只要是最后一步操作即可
function f(x) {
if (x > 0) {
return a(x)
}
return b(x);
}
函数a和b都属于尾调用,因为它们都是函数f的最后一步操作
函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"
例:阶乘
常规的计算阶乘的递归:
int f(int n) {
if (n == 1) {
return 1;
}
return f(n-1) * n;
}
空间复杂度为O(n)
尾递归:
int f(int n,int sum) {
if (n == 1) {
return sum;
}
return f(n-1, n * sum); //即:把sum独立出来,当做参数传递
}
空间复杂度为O(1)
斐波那契数列的尾递归
1 1 2 3 5 8 13 21 34 55 89。。。。。。
求斐波那契数列的第n的数(第0个数为1,第1个数为1,第2个数为2...)
一般递归代码:
public int F(int n) {
return n < 2 ? 1 : F(n - 1) + F(n - 2);
}
//F(5)
由于是递归调用,每次调用F函数的时候,会导致F(n)重复计算。因为,每个值最终被拆解为 F(1)+F(0).
尾递归代码:
public static int F(int n,int a1,int a2) {
return n == 0 ? a1 : F(n - 1, a2, a1 + a2);
}
//F(5,1,1)
我们在看尾递归的调用:F(5,1,1)=F(4,1,2)=F(3,2,3)=F(2,3,5)=F(1,5,8)=F(0,8,13)
所以,当我们调用F(5,1,1)的时候相当于变相的调用了F(0,8,13),正如上文中所说 :当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。 因为后续的方法并不依赖于之前的方法。