场景
今天去面试futu, 被问到一题 -> 参数为数字, 输出斐波那契数列对应结果, 实现了一下
function fibonacci(n) {
if (n == 1 || n == 2) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
然后面试官问: "你觉得这个有什么问题 ?"
我: "(⊙o⊙)…忘记判断参数类型了."
面试官: "不是这个问题."
我: "嗯..嗯...嗯.呃...这个..."
面试官: "如果数字很大会怎么样?"
我: "会...性能很差"
面试官: "嗯, 会爆炸, 为什么? 那怎么改?"
我: "嗯..嗯...嗯.呃...不会..."
面试官: "你回去查一下吧."
Game Over
回来查
原来 有个概念叫尾调用优化, 果然还是太菜
什么是尾调用
尾调用的概念非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。尾调用-阮一峰
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
我们知道,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。
递归的计算过程(recursive process)包含了两个阶段,先逐级扩展(expansion),构造起一个由被推迟的操作组成的链条(会被解释器保存在堆栈里),然后在收缩(contraction)阶段逐级回溯执行那些操作。随着递归计算步骤的增多,这种方法消耗的资源会越来越大,而且会包含越来越多的冗余操作,上面那个求斐波那契数列的例子(在SICP里被称作“树形递归”)在这方面问题尤其严重,因为它的计算步骤会随着参数而指数性的增长。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
优化->尾调用
function lastFibonacci(n, acc1, acc2) {
if(n == 1) return acc1
return lastFibonacci(n - 1, acc2, acc1 + acc2);
}
lastFibonacci(6,1,1) //8
lastFibonacci(7,1,1) //13
这样每次都要输入1,1,可以用柯里化或es6
加柯里化或es6
// 再封装一层柯里化-----------
function curringF(acc1, acc2){
return function(n){
return lastFibonacci(n, acc1, acc2);
}
}
let func = curringF(1,1)
func(6) // 8
func(7) // 13
//或 es6---------
function lastFibonacci(n, acc1=1, acc2=1) {
if(n == 1) return acc1
return lastFibonacci(n - 1, acc2, acc1 + acc2);
}
lastFibonacci(7) //13