斐波那契數列的幾種求解方式和複雜度分析
現在我們去面試,面試官要求我們使用Java寫出求解斐波那契數列指定項的函式,可能乍一聽很簡單,我們在大一的c語言課上就學過遞迴求解斐波那契數列的指定項,於是大筆一揮,寫下如下的第一種解法:
一、遞迴求解斐波那契數列
恩,我們考慮了程式碼的魯棒性,考慮了n小於0時候的特殊處理,我們也能通過這個函式算出斐波那契數列指定項,但是面試官一定不會滿意。我們來分析以下這個程式碼的缺點:public static long fibonacci(int n){ if(n<0) throw new IllegalArgumentException("Illegal Index"); else if(n == 0) return 0; else if(n == 1) return 1; else return fibonacci(n-1)+fibonacci(n-2); }
1、遞迴本質上是棧,當引數達到一定大小的時候會發生棧移出;
2、從演算法實現上來分析,我們要計算fibonacci(n)就需要計算fibonacci(n-1)和fibonacci(n-2),計算fibonacci(n-1)需要計算fibonacci(n-2)和fibonacci(n-3),計算fibonacci(n-2)需要計算fibonacci(n-3)和fibonacci(n-4)......實際上把這個遞迴演算法的遞迴樹畫出來,我們會發現很多樹結點是重複,也就是我們本來可以不重複計算那麼多次;
3、從時間複雜度上來分析,很明顯n時的複雜度:f(n)=f(n-1)+f(n-2) 為數學上的二階常係數差分方程,並且為齊次方程。它的時間複雜度為Ω(ф^n),其中ф為黃金分割數(√5 + 1)/2,Ω表示演算法複雜度最小是這麼多。也就是說這個演算法的複雜度為指數級的複雜度,指數級的時間複雜度我們怎麼能容忍?
因此這樣的演算法只適合幫助我們理解遞迴,不適合用來真正求解斐波那契數列。
二、從下往上計算,根據f(0)和f(1)計算出f(2),根據f(1)和f(2)計算出f(3),直到計算出第n項
使用這種方式,我們杜絕了大量的重複計算,使用中間值把需要的前兩次計算的結果儲存起來,下次要用直接查詢使用,而不是再次計算。這樣優化之後的演算法時間複雜度為O(n),也就是最多為n,明顯比遞迴要好上不少。但是到這裡就結束了嗎?不是的,我們還有第三種思路。public static long fibonacci(int n){ if(n < 0){ throw new IllegalArgumentException("Illegal Index"); } else if(n == 0) return 0; else if(n == 1) return 1; long fibnoacciNMinusOne = 1; long fibnoacciNMinusTwo = 0; long fibN = 0; for(int i = 2; i<=n; i++){ fibN = fibnoacciNMinusOne + fibnoacciNMinusTwo; fibnoacciNMinusTwo = fibnoacciNMinusOne; fibnoacciNMinusOne = fibN; } return fibN; }
三、利用數學公式的時間複雜度O(logn)的演算法
在程式碼實現之前,我們需要推導一個公式,首先來看一下斐波那契數列的遞推公式:
根據這個公式我們可以得到一個公式:
而且當n=2的時候,以上的公式也是成立的,因此上面的公式在n>=2的時候總成立,所以我們可以得到另一個公式:
我們可以用這個公式來計算斐波那契數列。如果我們從0開始,直到n-1計算矩陣的n-1次冪,時間複雜度仍然是O(n),並不比上面的第二種演算法快,我們先實現這種O(n)複雜度的演算法:
public static long fibonacci(int n){
if(n<0)
throw new IllegalArgumentException("Illegal Index");
long[][] basicMatrix = {{1,1},{1,0}};
long[][] matrix = calPowerOfMatrix(basicMatrix, n-1);
return matrix[0][0];
}
private static long[][] calPowerOfMatrix(long[][] matrix, int n){//計算matrix與{{1,1},{1,0}}矩陣的n次冪的乘積
for(int i = 0; i<n-1 ; i++){
long a = matrix[0][0];
long b = matrix[0][1];
long c = matrix[1][0];
long d = matrix[1][1];
matrix[0][0] = a + b;
matrix[0][1] = a;
matrix[1][0] = c + d;
matrix[1][1] = c;
}
return matrix;
}
但是乘方是具有以下性質的:
利用這個性質,我們可以利用這個公式使用二分法來遞迴實現斐波那契數列的求解,複雜度可以達到O(logn):
private static long[][] calPowerOfMatrix(long[][] matrix, int n) {
if (n < 0)
throw new IllegalArgumentException("Illegal Index");
else if (n == 1)
return matrix;
else if (n % 2 == 0)// n為偶數
// 如果n為偶數,就計算matrix的n/2次冪的2次冪
return calSquareOfMatrix((calPowerOfMatrix(matrix, n >> 1)));
else// n為奇數
// 如果n為奇數,就計算matrix的n/2次冪的2次冪再乘以matrix
return calBasicMutipyOfMatrix(calSquareOfMatrix(calPowerOfMatrix(matrix, (n - 1) >> 1)));
}
private static long[][] calBasicMutipyOfMatrix(long[][] matrix) {// 計算matrix與{{1,1},{1,0}}的乘積
long a = matrix[0][0];
long b = matrix[0][1];
long c = matrix[1][0];
long d = matrix[1][1];
matrix[0][0] = a + b;
matrix[0][1] = a;
matrix[1][0] = c + d;
matrix[1][1] = c;
return matrix;
}
private static long[][] calSquareOfMatrix(long[][] matrix){//計算matrix的平方
long a = matrix[0][0];
long b = matrix[0][1];
long c = matrix[1][0];
long d = matrix[1][1];
matrix[0][0] = a*a + b*c;
matrix[0][1] = a*b + b*d;
matrix[1][0] = a*c + c*d;
matrix[1][1] = b*c + d*d;
return matrix;
}
public static long fibonacci(int n) {
if (n < 0)
throw new IllegalArgumentException("Illegal Index");
long[][] basicMatrix = { { 1, 1 }, { 1, 0 } };
long[][] matrix = calPowerOfMatrix(basicMatrix, n - 1);
return matrix[0][0];
}
這樣就利用乘方的特性和斐波那契遞推公式得出的公式寫出了基於遞迴二分法複雜度為O(logn)的演算法,這樣才應該是最優的解法。