首先解釋下什麼是斐波那契數列:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, ...
在種子數字 0 和 1 之後,後續的每一個數字都是前面兩個數字之和。
解法1:
function fibonacci(n) {
if(n==0 || n == 1)
return n;
return fibonacci(n-1) + fibonacci(n-2);
}
以上函數使用遞歸的方式進行斐波那契數列求和,我們可能首先想到用這個,使用遞歸計算大數字時,性能會非常低;
解法2:
上面遞歸執行太多相同運算,我們可以對中間求得的變量值,進行存儲的話,就會大大減少函數被調用的次數。
let fibonacci = function() {
let arr= [0, 1];
return function(n) {
let result = arr[n];
if(typeof result != 'number') {
result = fibonacci(n - 1) + fibonacci(n - 2);
arr[n] = result; // 將每次 fibonacci(n) 的值都緩存下來
}
return result;
}
}(); // 執行函數
解法3:遞推法
function fibonacci(n) {
let current = 0;
let next = 1;
let temp;
for(let i = 0; i < n; i++) {
temp = current;
current = next;
next += temp;
}
console.log(`fibonacci(${n}, ${next}, ${current + next})`);
return current;
}
從下往上計算,首先根據f(0)和f(1)算出f(2),再根據f(1)和f(2)算出f(3),依次類推我們就可以算出第n項了。比遞歸的效率高很多。
上述還可以借用解構賦值省略temp中間變量
function fibonacci(n) {
let current = 0;
let next = 1;
for(let i = 0; i < n; i++) {
[current, next] = [next, current + next];
}
return current;
}
解法4:尾調用優化
// 在ES6規範中,有一個尾調用優化,可以實現高效的尾遞歸方案。
// ES6的尾調用優化只在嚴格模式下開啓,正常模式是無效的。
'use strict'
function fib(n, current = 0, next = 1) {
if(n == 0) return 0;
if(n == 1) return next; // return next
return fib(n - 1, next, current + next);
}
解析:
什麼是尾調用?
尾調用(Tail Call)是函數式編程的一個重要概念,本身非常簡單,一句話就能說清楚,就是指某個函數的最後一步是調用另一個函數。
function f(x){
return g(x);
}
尾調用優化?
函數調用會在內存形成一個“調用記錄”,又稱“調用幀”(call frame),保存調用位置和內部變量等信息。如果在函數A的內部調用函數B,那麼在A的調用幀上方,還會形成一個B的調用幀。等到B運行結束,將結果返回到A,B的調用幀纔會消失。如果函數B內部還調用函數C,那就還有一個C的調用幀,以此類推。所有的調用幀,就形成一個“調用棧”(call stack)。
尾調用由於是函數的最後一步操作,所以不需要保留外層函數的調用幀,因爲調用位置、內部變量等信息都不會再用到了,只要直接用內層函數的調用幀,取代外層函數的調用幀就可以了。
function g(item) {
return item
}
// 下面是尾調用例子
function f() {
let m = 1
let n = 2
return g(m + n)
}
f()
// 上面例子實際上等同於:
g(3)
上面代碼中,如果函數g不是尾調用,函數f就需要保存內部變量m和n的值、g的調用位置等信息。但由於調用g之後,函數f就結束了,所以執行到最後一步,完全可以刪除f(x)的調用幀,只保留g(3)的調用幀。
這就叫做“尾調用優化”(Tail call optimization),即只保留內層函數(即g函數)的調用幀。如果所有函數都是尾調用,那麼完全可以做到每次執行時,調用幀只有一項,這將大大節省內存。這就是“尾調用優化”的意義。
尾遞歸是什麼?
遞歸相信大家都聽過,函數調用自身,稱爲遞歸,如果尾調用自身,就稱爲尾遞歸。
(遞歸非常耗費內存,因爲需要同時保存成千上百個調用幀,很容易發生“棧溢出”錯誤(stack overflow)。但對於尾遞歸來說,由於只存在一個調用幀,所以永遠不會發生“棧溢出”錯誤。)
function fib(n, current = 0, next = 1) {
if(n == 0) return 0;
if(n == 1) return next; // return next
return fib(n - 1, next, current + next);
}