Fibonacci
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……
原始解法
function fibo(n) {
if (n < 2) return n;
return fibo(n - 1) + fibo(n - 2);
}
最简单易懂,但是性能最差,不断调用自身函数的过程会重复地产生同一个函数内容的调用堆栈,并且会重复计算一些已经计算过的结果。空间复杂度O(n²)
尾递归优化
function fibonacci(n, current, next) {
if (n === 1) return next;
if (n === 0) return 0;
return fibonacci(n - 1, next, current + next);
}
当一个函数返回的结果是一个函数的返回值,同时在返回的时候没有其他的语句(即除了该返回的函数返回值以外函数内其他的语句运行结果都可以丢弃时),称为尾调用。 如果这个函数恰好是自身函数的话,称为尾递归。 在ES6的严格环境下,尾递归会被自动复用当前的调用帧,不再重复堆叠相同的函数调用堆栈,空间复杂度是O(1)
14.6 Tail Position Calls The abstract operation IsInTailPosition with argument nonterminal performs the following steps:
- Assert: nonterminal is a parsed grammar production.
- If the source code matching nonterminal is not strict code, return false.
- If nonterminal is not contained within a FunctionBody or ConciseBody, return false.
- Let body be the FunctionBody or ConciseBody that most closely contains nonterminal.
- If body is the FunctionBody of a GeneratorBody, return false.
- Return the result of HasProductionInTailPosition of body with argument nonterminal.
_ Tail Position calls are only defined in strict mode code because of a common non-standard language extension (see 9.2.7) that enables observation of the chain of caller contexts.
具体判断是否为尾递归可以参阅 ecma-262/14.6 Tail Position Calls
递推法
除了递归,还可以用循环的方法来递推,按照 Fibonacci[n] = Fibonacci[n-1] + Fibonacci[m-2] 的规律很自然地可以写出for循环的代码,但是需要先给出第0和第1位数。
function fibonacci(n) {
const arr = [0, 1];
for (let i = 2; i <= n; i++) {
arr[i] = arr[i - 1] + arr[i - 2];
}
return arr[n];
}
但是这样的话就会保存着n-1个我们结果并不需要的计算中间值,可以用一个临时变量来解决:
function fibonacci(n) {
let current = 0
let next = 1
let toAdd
for (let i = 0; i < n; i++) {
toAdd = current
current = next
next += toAdd
}
return current
}
临时变量的写法还是不够优雅,然而ES6的解构赋值完美地解决了这个问题:
function fibonacci(n) {
let current = 0;
let next = 1;
for (let i = 0; i < n; i++) {
[current, next] = [next, current + next];
}
return current;
}
for循环还是太啰嗦了,用while来优化一下:
function fibonacci(n) {
let current = 0;
let next = 1;
while (n-- > 0) {
[current, next] = [next, current + next];
}
return current;
}
这种按照一个数组逐个元素累积计算得到的答案,自然让我联想到ES6的Array.prototype.reduce方法,再来改写一下:
function fibonacci(n) {
let toAdd = 1;
return [...Array(n)].reduce(prev => {
let res = prev + toAdd;
toAdd = prev;
return res;
}, 0); // 空元素的数组不能直接reduce,要传入一个initialValue😂
}
通项公式
通过百度百科,查阅到斐波那契数列的通项公式:
🙈于是程序实现:
function fibonacci(n) {
const SQRT5 = Math.sqrt(5);
return Math.round((1 / SQRT5) *(Math.pow((1 + SQRT5) / 2, n) - Math.pow((1 - SQRT5) / 2, n)));
}