拜託,面試別再問我斐波那契數列了!!!
面試中,問得比較多的幾個問題之一,求斐波那契數列f(n)?
畫外音:姐妹篇
《 ofollow,noindex">拜託,面試別再問我TopK了!!! 》
《 拜託,面試別再讓我數1了!!! 》
什麼是斐波那契數列?
斐波那契數列是這樣一個數列,它滿足:
f(0) = 0;
f(1) = 1;
f(n) = f(n-1) + f(n-2) (當n>=2時)
到底有幾種方法,這些思路里蘊含的優化思路究竟是怎麼樣的,今天和大家聊一聊。
一、遞迴法
虛擬碼 :
uint32_t f(uint32_t n){
if(n==0) return 0;
if(n==1) return 1;
return f(n-1)+f(n-2);
}
思路 :這是一個遞迴的程式碼,非常清晰,直接把斐波那契數列的定義翻譯成了程式碼。
例如 :
假設要求f(5)
f(5) = f(4) + f(3);
於是會遞迴計算f(4)和f(3);
接著要求f(4)
f(4) = f(3)+ f(2);
於是會遞迴計算f(3)和f(2);
可以看到,計算f(5)和f(4)中都要計算f(3),但這 兩次f(3)會重複計算 ,這就是遞迴的最大問題,對於同一個f(a),不能複用。
計算一個f(n)到底需要有多少次遞迴呼叫呢?
我們可以在程式碼裡 加一個計數 驗證一下。
虛擬碼 :
static uint32_t count=0; // 加一個全域性變數計數
uint32_t f(uint32_t n){
count++; // 遞迴一次,計數加一
if(n==0) return 0;
if(n==1) return 1;
return f(n-1)+f(n-2);
}
實驗的結果 :
f(5) count = 15
f(10) count = 177
f(15) count = 1K+
f(20) count = 2W+
f(25) count = 24W+
f(30) count = 269W+
f(35) count = 2986W+
f(40) count = 3.3Y+
f(45) … 抱歉,我機器太慢,算不出來
額滴神哪,不是騙我吧!!!
畫外音:
(1)這個count,是函式遞迴了對少次;
(2)f(45),機器居然算不出來;
(3)對結論有疑問的,自己可以run一把;
啟示 :
(1)斐波那契數列求解,如果用直接法, 時間複雜度是指數級的 ,不可行;
(2)如果沒有太大的把握, 工程中儘量少使用遞迴 ,容易把自己玩死;
二、正推法
從斐波那契數列的定義:
f(0) = 0;
f(1) = 1;
f(n) = f(n-1) + f(n-2) n>=2時
可以看出, 每一個新的f(n),是前兩個舊的f(n-1)和f(n-2)之和 ,一路遞迴下去,最終都將遞迴到f(0)和f(1)上來。
反過來想,我們不倒著f(n),f(n-1),f(n-2)這麼計算,而是f(0),f(1),f(2)…f(n)這麼 正向計算 ,豈不是更快麼?
虛擬碼 :
uint32_t f(uint32_t n){
uint32_t arr[n];
arr[0]=0;
arr[1]=1;
for(uint32_t i=2;i<=n;i++){
arr[i]=arr[i-1]+arr[i-2];
}
return arr[n];
}
這麼正向的計算,只需要一個for迴圈,就能夠計算出f(n)的值, 時間複雜度是O(n) 。
三、通項公式法
f(0) = 0;
f(1) = 1;
f(n) = f(n-1) + f(n-2) (當n>=2時)
大學學過相關課程,可解出f(n)通項公式。
畫外音:額,是不是有朋友讀了個假大學。
線性遞推數列 :
f(n) = f(n-1) + f(n-2)
對應的 特徵方程 是:
x^2 = x + 1
求解特徵方程得到:
x1=(1+√5)/2
x2=(1-√5)/2
於是得到:
f(n) = a1(x1)^n + a2(x2)^n
將:
f(0) = 0;
f(1) = 1;
代入上述通項公式。
於是得到:
a1=1/√5
a2=-1/√5
於是最終得到 :
f(n)=(1/√5)*{[(1+√5)/2]^n -[(1-√5)/2]^n}
畫外音:別問我為什麼懂這些,我TM作為計算機資訊保安專業,也被數論,有限域,加密解密這些數學學科折磨過。
可忽略上述吹牛*的過程,百度一下能得到答案。
總之,得到了斐波那契數列通項公式:
f(n) = a1(b1)^n + a2(b2)^n
其中a1, b1, a2, b2四個數字都是常數。
想求f(45),把n=45帶入上述通項公式即可。
那麼,帶入通項公式求解,時間複雜度是多少呢?是O(1)麼?
通項公式的計算,並不能O(1)得到,而是一個a^n,即power(a, n)的求解過程。
那麼,如何求解a的n次方呢?
最粗暴的方法,將a不斷的自乘n次。
虛擬碼 :
uint64_t power(uint64_t a, uint64_t n){
uint64_t result=a;
for(uint64_t i=1;i<n;i++){
result *=a;
}
return result;
}
很容易知道,a通過for迴圈不斷自乘,求解a^n的 時間複雜度是O(n) 。
你TM在逗我!!!
通過“正推”法,求解f(n)的時間複雜度是O(n)。
樓主搞了這麼久的奇技淫巧,搞什麼“通項公式法”,結果也是個O(n)的方法???
不不不,稍安勿躁,上面講的都是思路,求解a^n,可以使用之前文章裡說過的 減治法 。
還記得分治法與減治法的區別麼?
● 減治法,大問題分解為小問題,小問題 只要 遞迴一個分支,例如:二分查詢,隨機選擇
● 分治法。大問題分解為小問題,小問題 都要 迭代各個分支,例如:快速排序
具體在《 拜託,面試別再問我TopK了!!! 》裡,講 隨機選擇 時詳細介紹過。
a^n 減治法思路 :
● 當n是 偶數時 ,先求出 r=a^(n/2) ,再做一次r*r的計算,就得到了a^n
,在做一次r*r*a,就得到了a^n
虛擬碼 :
uint64_t power(uint64_t a, uint64_t n){
if(n==0)return 1;
if(n==1)return a;
uint64_t r=0;
if(n%2){
r=power(a, (n-1)/2);
return r*r*a;
}
else{
r=power(a, n/2);
return r*r;
}
}
每次將計算規模減半,是不是和二分查詢很像?減治法求a^n的 時間複雜度是O(lg(n)) 。
四、查表法
通過之前幾篇演算法文章的套路,大家應該猜得到,一到結尾, 空間換時間 的方法就出場了,如果有相對充裕的記憶體,可以有更快的演算法。
思路 :f(n),一旦n的值確定,f(n)也就確定,可以 提前計算好結果陣列 :
result[0]=0;
result[1]=1;
result[2]=1;
result[3]=2;
result[4]=3;
…
求f(n)時直接打表即可,虛擬碼:
return result[n];
打表的時間複雜度是O(1)。
畫外音:但每期都講打表,太沒有意思了。
五、總結
斐波那契數列,不難;
但其思路有優化過程,並不簡單:
● 遞迴法,f(45)能跑得宕機
● 正推法,O(n),正推計算,有點意思
● 通項公式,本質轉化為求a^n
● 減治法求a^n,O(lg(n))