从菲波那切数列看尾部调用优化
1、菲波那切数列
在数学上,斐波那契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=Fn-1+Fn-2(n>=2,n∈N*),用文字来说,就是斐波那契数列列由 0 和 1 开始,之后的斐波那契数列系数就由之前的两数相加。形如:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……
数学上的计算公式是:
读书时候还有利用线性方程推算过这个公式,不过现在都忘了差不多了 ~~泪奔~~。
其实用代码描述兔子生娃的故事也没少干,常见算法有递归法和递推法。
2、递归
function fibonacci(n){ if(n === 1 || n === 0 ) return n; return fibonacci(n-1) + fibonacci(n-2); }
说明:
递归造成了大量的重复计算,使用递归计算大数字时,性能会特别低。
函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。因而,当递归层数过大之后,就可能造成调用栈占用内存过大或者溢出。
3、递推法
function fibonacci(n) { let current = 0; let next = 1; for(let i = 0; i < n; i++){ [current, next] = [next, current + next]; } return current; }
4、 尾调用优化
尾调用优化是指某个函数的最后一步是调用另一个函数。
最简单模式:
function f(x){ return g(x); }
结合上面的递归说明,尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
菲波那切数列改写成尾调用写法:
'use strict' function fibonacci(n, current = 0, next = 1) { if(n === 1) return next; if(n === 0) return 0; return fibonacci(n - 1, next, current + next); }
5、动态规划
function fibonacci(n) { var n1 = 1, n2 = 1, sum; for (let i = 2; i < n; i++) { sum = n1 + n2 n1 = n2 n2 = sum } return sum }
参考:阮一峰--《尾调用优化》