1. 程式人生 > >斐波那契數列時間複雜度和通項公式的一些記錄

斐波那契數列時間複雜度和通項公式的一些記錄

       真沒啥好說的QAQ。但是前陣子公司的技術BOSS和咱糾結這個數列的一些問題,於是我只好記錄一下一些東西,表示自己還是學到了一點東西滴~

       話說真查便發現這玩意鼎鼎大名無處不在,反正我高數不好大部分看不懂,就覺得不明覺厲……

       所謂斐波那契數列就是指1 1 2 3 5 8 ....這一數列。數列的格式是F(1)=1 ,F(2)=1,F(n)=F(n-1)+F(n-2)(n>=3)。 斐波那契數列原型是個非常典型的遞迴演算法,根據上述定義可以很簡單的寫出求該數列第n位的遞迴演算法為:

int fib(int n){   
   if(n>=2){       
      return 1;    
   }    
   return fib(n-1)+fib(n-2);
}

         這個醜陋的函式十分鮮明滴證明了一個道理,那就是遞迴演算法嚴重影響計算效能=-=。我自己實際運行了一下,電腦在計算到第43位數字時開始耗費大量的時間:

計算位數32結果2178309計算次數4356617套O(n)2178309
計算位數33結果3524578計算次數7049155套O(n)3524578
計算位數34結果5702887計算次數11405773套O(n)5702887
計算位數35結果9227465計算次數18454929套O(n)9227465
計算位數36結果14930352計算次數29860703套O(n)14930352
計算位數37結果24157817計算次數48315633套O(n)24157817
計算位數38結果39088169計算次數78176337套O(n)39088169
計算位數39結果63245986計算次數126491971套O(n)63245986

計算位數40結果102334155計算次數204668309套O(n)102334155

        可以看到當計算數列第40位即F(40)時,這個函式已經需要執行2億多次。

       據說在代數裡這種本項等於前兩項之和的式子叫做二階齊次差分方程(http://bbs.pediy.com/archive/index.php?t-123051.html)?!數學上這個數列的通項公式可以根據齊次差分方程的求解公式獲得。於是該數列第n位的值F(n)就是:

      得到通項公式也差不多就可以知道演算法的時間複雜度了。於是上面這個函式的演算法複雜度是指數級的。

      時間複雜度的衡量一般都是按級別劃分,從小到大依次為O(1),O(logn),O(n),O(n^a),O(a^n)和(n!)。指數級的演算法是不可接受的,基本上很難在實際場合中運用。log級別的則一般是最為理想的。這個數列當然也有log級別的演算法,是利用矩陣相乘原理得到的。這個網上證明很多,我不貼了,貼了還是看不懂- -。

      在上面我貼了一段函式執行結果,套O(n)後面的數字其實就是利用該函式的通項公式獲得。計算次數則是呼叫了上述函式的次數。可以發現函式呼叫次數正好是F(n)的2倍少1.關於這個也很好解釋。舉個例子,比如求F(6),那麼函式的執行情況如下圖所示:

     

      可以看出這是一顆二叉樹,每個節點表示函式被運行了一次。F(2)=F(1)=1 ,所以執行到F(2)時函式即可返回值。可以發現樹的葉子節點的個數就是F(6)的值,它和通項公式獲得的解是一致的。而非葉子節點的個數則比葉子節點數少1.因為葉子節點最終會彙集到根節點,根節點只有1個,葉子節點有n個,而二叉樹每次彙集會減少一個葉子節點,所以非葉子節點的數量比葉子節點數少1,因此該函式的執行次數就是2F(n)-1。也就是說該函式的時間複雜度是通項公式的2倍少1.

      我是覺得際計算機執行時是不可能用通項公式去計算數列某位的值的,因為這個通項公式裡有無理數,必然造成計算結果不精確。後來網上資料也顯示正確的計算機演算法就是之前說的那個矩陣相乘法。

       BOSS說和我們糾結這個問題是因為將來如果我們成長到要寫某些系統的核心演算法時,會遇到這種優化遞迴演算法的問題。優化演算法有這麼幾種思路,一是從演算法思想上進行變化,找到一種計算次數更少的演算法,這是演算法上的改進;二是對程式碼進行優化,將遞迴演算法改寫成迴圈迭代,會大大減少空間佔用等問題,而當演算法很難改寫成迭代形式時,可以嘗試將程式碼改寫成“尾遞迴”。我是覺得叫“偽遞迴”不是更形象麼。。

      尾遞迴就是從最後開始計算, 每遞迴一次就算出相應的結果, 也就是說, 函式調用出現在呼叫者函式的尾部, 因為是尾部, 所以根本沒有必要去儲存任何區域性變數. 直接讓被呼叫的函式返回時越過呼叫者, 返回到呼叫者的呼叫者去.

       偽遞迴,哦,是尾遞迴-。-。 尾遞迴可以大大減少程式記憶體棧的使用率,從而在對程式碼進行更改的難度和演算法效率上得到了折中,應該算是一種中庸思想吧……

       在斐波那契數列這個問題上用尾遞迴顯得沒必要,因為即使寫成迭代for迴圈的形式也很簡單,但將來或許會碰上一些難以用簡單形式表達的演算法,那麼這時候尾遞迴就會顯示出它的優越性了。

       斐波那契數列的定義寫成函式,其實是一種自頂向下的計算過程,其中有大量的計算重複,從上面的樹圖中就能發現,F(3)和F(4)都計算了多次。有種比較好的思路是自底向上,這種思想應該說事動態規劃的思想。像斐波那契數列中自底向上的演算法就可以方便的寫成迴圈迭代形式,空間效率和時間效率都比遞迴演算法好很多:

int n;
int i;
int S0=1;
int S1=1;
for(i=3;i<=n;i++){
   S1=S0+S1;
   S0=S1-S0;
}
          S1就是數列第n位的值。