根據高德納(Donald Ervin Knuth)的《計算機程序設計藝術》(The Art of Computer Programming),
1150年印度數學家Gopala和金月在研究箱子包裝物件長寬恰好爲1和2的可行方法數目時,首先描述這個數列。
在西方,最早研究這個數列的人是比薩的列奧那多(意大利人斐波那契Leonardo Fibonacci),
他描述兔子生長的數目時用上了這數列:javascript
- 第一個月初有一對剛誕生的兔子
- 第二個月以後(第三個月初)它們能夠生育
- 每個月每對可生育的兔子會誕生下一對新兔子
- 兔子永不死去
假設在n月有兔子總共a對,n+1月總共有b對。在n+2月一定總共有a+b對:
由於在n+2月的時候,前一月(n+1月)的b對兔子能夠存留至第n+2月(在當月屬於新誕生的兔子尚不能生育)。而新生育出的兔子對數等於全部在n月就已存在的a對html
費波那契數列由0和1開始,以後的費波那契係數就是由以前的兩數相加而得出:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233……java
若是用數學語言來描述大概是下面這個樣子:
$$F_0 = 0$$
$$F_1 = 1$$
$$Fn = F(n-1) + F_(n-2)$$
python
學過編程的人,第一反應確定是用遞歸求解:程序員
def fib(n):
assert n >= 0, 'input invalid'
return n if n<=1 else fib(n-1) + fib(n-2)複製代碼
function fib(n) {
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}複製代碼
遞歸的好處就是代碼清晰明瞭,寫起來乾淨利索,絲毫沒有拖泥帶水的感受算法
這種遞歸求解的方法的過程能夠簡化爲以下圖所示的二叉樹:
編程
下面是我統計不一樣語言用遞歸算法求解斐波那契數列第41項所需的時間:數組
ps:這裏直接用的系統自帶的
time
命令來統計的運行時間等信息性能優化
因爲龜叔認爲程序員根本用不到遞歸,因此一直拒絕爲python加上尾遞歸優化,甚至當遞歸深度超過1000時,直接拋出RuntimeError: maximum recursion depth exceeded
具體內容請參見:Tail Recursion Eliminationapp
那篇09年的博客裏是這樣說的:
我不認爲遞歸是編程的基礎。遞歸是一些計算機科學家們,尤爲是那些熱愛Scheme (lisp的一支)和喜歡用‘cons’ 來教表頭表尾和遞歸的人們。
可是對我(Guido)來講,遞歸只是一些爲基礎數學研究而存在的理論手段(例如分形幾何學),而不是平常的編程工具。
Python的哲學是「作一件事情有且只有一種方法」(There should be one-- and preferably only one --obvious way to do it.)
龜叔堅持不給Python加上尾遞歸的優化偏偏體現了這種哲學,這個設計哲學不只減輕了人們在開發時的認知負擔和選擇成本,對於提升開發效率是頗有幫助的。
同時,這個特色使得不一樣的人用Python寫出來的代碼不至於相差很大,這對於團隊合做也是頗有用的。
因此咱們的斐波那契數列固然不能直接用遞歸求解啦,比較常見的思路是把遞歸改成遞推,把斐波那契的前兩項先初始化爲數組,
而後根據f(n) = f(n-1) + f(n-2)
用循環一次算出後面的每一項,這種算法的時間複雜度爲O(n)。
我在個人電腦上測了一下,下面這段代碼求第41項只用了0.02秒。
def fast_fib(n):
f = [0, 1]
for i in range(2, n+1):
f.append(f[i-1] + f[i-2])
return f[n]複製代碼
比較一下遞歸法和遞推法:
兩者都用了分治的思想——把目標問題拆爲若干個小問題,利用小問題的解獲得目標問題的解。
兩者的區別實際上就是普通分治算法和動態規劃的區別。
其實還有一個更加巧妙的辦法(利用通項公式求解除外)
咱們先把斐波那契數列中相鄰的兩項:F(n)和F(n - 1)寫成一個2x1的矩陣,而後對其進行變形:
繼續推導能夠獲得:
利用矩陣來運算的話,整個算法的時間複雜度是O(log n),空間複雜度是O(1)
斐波那契數列通項公式的推導也是個頗有意思的題目,能夠利用生成函數來推導,這裏就不展開了
原文連接: 從斐波那契數列談談代碼的性能優化