轉載請申明,轉自【http://www.javashuo.com/article/p-gyeuuhbb-bd.html】,謝謝!html
斐波那契數列指的是這樣一個數列 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368......java
我記得在初學C語言的時候,大學老師常常會講一些常見的數學問題及遞歸的使用,其中斐波那契數列就是必定會被拿出來舉例的。在後來工做中,面試作面試題的時候,也很大機率會出現編寫算法實現斐波那契額數列求值。能夠說,在咱們編程道路上,編寫算法實現斐波那契數列是每一個程序員一定會作的一件事。昨天去參加騰訊課堂舉辦的一個線下活動,活動中有一位嘉賓,是某課堂的創始人,也是算法課程的講師,就講到了這個問題,算是顛覆了我對該問題的認知。本文就根據這名講師的講解,來分析和整理一下該問題算法的實現。程序員
下面,咱們來看看其中幾種常見的算法,並分析其效率。面試
一、遞歸法算法
經過觀察,咱們會發現其中的規律,第一項和第二項的值均爲1,後面每項的值都是前兩項值之和,因此咱們不少人基本上都會使用遞歸來實現,常見的算法以下:編程
1 public int fib(int n) { 2 if (n == 1 || n == 2) { 3 return 1; 4 } 5 return fib(n - 2) + fib(n - 1); 6 }
這段代碼看起來很是的簡潔和優雅,我想咱們絕大多數的人平時也都是這麼寫的吧,在此以前筆者就一直都是這麼寫的,並且在個人知識儲備中也就只知道有這樣一種算法。數組
實際上,當n還比較小的時候,用遞歸法來實現是沒有什麼問題的,可是當n稍微大一點的時候,好比n=45的時候,咱們經過以下測試代碼來看看它的執行結果:測試
1 MyClass myClass = new MyClass(); 2 long t1 = System.currentTimeMillis(); 3 int n = 45; 4 int result = myClass.fib(n); 5 long t2 = System.currentTimeMillis(); 6 System.out.println("n=" + n + ";result=" + result + ";time=" + (t2 - t1));
獲得結果爲:spa
n=45;result=1134903170;time=2881
咱們發現執行這段代碼,花費的時間是2881ms。若是值再大一點,如n=48:.net
n=48;result=512559680;time=11746
時間達到了11s以上了!若是n再稍微大一點,所消耗的時間是成指數級增加的,好比n=64的時候,所消耗的時間多是兩三百年!不信的話,讀者能夠試試!
這樣看來,就很是可怕了,咱們一直認爲毫無問題的看起來既簡潔又優雅的算法,竟然是這麼耗時的,這簡直就是一段垃圾代碼。
咱們用一張圖來簡單分析一下該算法的執行過程,以n=6爲例:
咱們會發現f(n)這個方法被調用了不少次,並且其中重複率很是之高,也就是說被重複計算了不少次,若是n稍微大一點這棵樹會很是龐大。這裏咱們能夠看出,每一個節點就須要計算一次,總計算的次數就是該二叉樹節點的數量,可見其時間複雜度爲O(2n),是指數級的,其空間複雜度也就是該二叉樹的高度,爲O(n)。這樣來看,咱們應該就清楚了,爲何這段代碼效率如此低下了吧。
二、數組保存法(該名稱是本身命名的,想不出什麼好名字了,不喜勿噴哈...)
爲了不無數次重複,能夠從n=1開始往上計算,並把每個計算出來的數據,用一個數組保存,須要最終值時直接從數組中取便可,算法以下:
1 public int fib(int n) { 2 int[] fib = new int[n]; 3 fib[0] = 1; 4 fib[1] = 1; 5 for (int i = 2; i < n; i++) { 6 fib[i] = fib[i - 2] + fib[i - 1]; 7 } 8 return fib[n - 1]; 9 }
咱們也分別取n=45和n=48來看看執行結果
n=45;result=1134903170;time=0
n=48;result=512559680;time=0
消耗的時間都是0(我這裏獲取的時間是精確到ms級別的,先後的時間差在1ms如下,因此這裏計算出來的結果爲0,實際耗時不可能爲0,後續不贅述),可見執行效率提升了不少。這種算法主要有一個for循環,其時間複雜度爲O(n),期間須要開闢一個長度爲n的數組,因此空間複雜度也爲O(n),這就在上述算法的基礎上極大地提高了效率。
三、滾動數組法(這個命名是讀者評論區提出來的,雖然不是很理解,不過仍是採納吧,總比我本身瞎命名好,感謝這位童鞋)
儘管上述算法已經很高效了,但咱們仍是會發現一個問題,其實整個數組中,每次計算時都只須要最新的3個值,前面的值計算完後就再也不須要了。好比,計算到第10次時,須要的數組空間只有第8和第9兩個空間,前面第1到第7個空間其實就再也不須要了。因此咱們還能夠改進,經過3個變量來存儲數據,算法以下:
1 public int fib(int n) { 2 int first = 1; 3 int second = 1; 4 int third = 2; 5 for (int i = 3; i <= n; i++) { 6 third = first + second; 7 first = second; 8 second = third; 9 } 10 return third; 11 }
時間複雜度仍然爲O(n),而空間複雜度爲常量級別3,即空間複雜度爲0,因此這種方法是很是高效的。
四、公式法
實際上,求斐波那契數列值有一個公式:
能夠經過該公式來實現算法:
1 public int fib(int n) { 2 double c = Math.sqrt(5); 3 return (int) ((Math.pow((1 + c) / 2, n) - Math.pow((1 - c) / 2, n)) / c); 4 }
其時間複雜度和空間複雜度就取決於JDK中這些數學公式的實現了,執行效率也是很是高的:
n=48;result=512559680;time=0
五、尾遞歸法
這裏先亮出該算法吧:
1 public int fib5(int n, int first, int second) { 2 if (n <= 1) { 3 return first; 4 } else { 5 return fib5(n-1,second,first+second); 6 } 7 }
其實我起初以爲這種方法的實現和第三種算法的中心思想挺相似的,雖然乍一看好像差異挺大,但仔細分析,也都是經過兩個變量保存計算值,傳遞給下一次進行計算,遞歸的過程當中也是根據n值變化逐步重複運算,和循環差很少,時間複雜度和空間複雜度也都同樣,因此最初就沒有單獨列出來。後來評論區有童鞋提出來了,仔細想一想,這種方式從形式上和第三種算法差異仍是挺大的,並且簡潔不少,優雅不少,也有一個響亮的名字,仍是單獨列出來更好,這裏也感謝評論區童鞋們的寶貴意見。
六、矩陣快速冪法
本方法是在評論中才知道的,我也在其網上搜了一下該方法,惋惜當年學的矩陣知識都又還給老師了,一時半會我是真看不懂,這裏我也就不打腫臉充胖子了,給個連接:https://blog.csdn.net/computer_user/article/details/86927209,有分析過程和完整的java實現代碼。
因爲筆者水平有限,文章中必定有不少須要改進的地方,若是讀者發現本文有描述不正確或者不妥的地方,請不吝賜教,很是感謝!另外,很是感謝評論區讀者們的寶貴意見!