铺垫
函数的调用会在内存中形成一个调用记录,也称作“调用帧”,用来保存调用位置和内部变量等信息。内存中用一个栈结构来保存调用帧,也称“调用栈”。函数被调用时调用帧入栈,函数返回时调用帧弹栈。
当发生函数的嵌套时,如在A函数中调用B函数,就会有连续的两个调用帧入栈
function A(){
...
B();
...
}
当B函数返回时,B调用帧弹栈,继续执行A函数,当A函数返回时,A调用帧弹栈
递归函数就是在函数内部不断调用自身,直到满足特定条件
递归函数的缺点
递归函数在函数内部不断调用自身,导致调用栈快速增大,这是很浪费内存资源的,而且调用栈都是有固定大小的,如果递归次数过多会造成栈溢出错误
递归函数优化--尾递归
什么是尾调用?
很简单,就是指某个函数的最后一步操作是调用另一个函数
//js
function A(x){
...
return B(x);
}
以下三种都不属于尾调用
//情况一
function A(x){
let y = B(x);
return y;
}
// 情况二
function A(x){
return B(x) + 1;
}
// 情况三
function A(x){
B(x);
}
情况一:调用函数B之后有赋值操作
情况二:调用函数B之后有运算操作
情况三:函数调用如果没有显式的return,编译器会隐式的 return undefined;所以他的最后一步不是调用函数B,而是return undefined
我们再来看一看尾调用的调用栈情况
function A(x){
...
return B(x);
}
A(1);
因为函数返回时调用帧会弹栈,所以A函数的最后一步操作 return B(x),这一行代码会让A调用帧弹栈,并且让B调用帧入栈,所以此时调用栈里只有一条记录。当把这一方法用到递归中时就是尾递归了
尾递归
如果保证所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这样就大大的节省了内存,而且不会出现栈溢出
接下来我们看看怎么把普通的递归函数改成尾递归,以递归求阶乘为例
普通递归函数
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) //120
尾递归
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
调用栈中只有一个调用帧,所以复杂度为O(1)