斐波那契數列(一)--對比遞迴與動態規劃(JAVA)
兔子繁殖問題:
這是一個有趣的古典數學問題,著名義大利數學家Fibonacci曾提出一個問題:有一對小兔子,從出生後第3個月起每個月都生一對兔子。小兔子長到第3個月後每個月又生一對兔子。按此規律,假設沒有兔子死亡,第一個月有一對剛出生的小兔子,問第n個月有多少對兔子?
相信上面的題目稍微有點經驗的程式設計師都瞭解過,這就是著名的斐波那契數列(Fibonacci sequence),該數列,又稱黃金分割數列、因數學家列昂納多·斐波那契(Leonardoda Fibonacci)以兔子繁殖為例子而引入,故又稱為“兔子數列”,指的是這樣一個數列:1、1、2、3、5、8、13、21、34、……
特點:這個數列從第3項開始,每一項都等於前兩項之和。
表示式:F[n]=F[n-1]+F[n-2] (n>=2,F[0]=0,F[1]=1)
看到這個表示式相信大多數讀者都能想到使用遞迴演算法實現,那麼由此我們可以得到求解斐波那契數列的一種演算法:
public class Main {
public static int f (int n) {
if (n <= 2) {
return 1;
} else {
return f(n-1) + f(n-2);
}
}
public static void main (String[] args) {
int n = 12;
System.out.println(f(n));
}
}
注:若用於求解兔子繁殖問題需要對輸入引數進行額外處理
但是!!!
對於小資料來說,上面的演算法或許還可行,但是我們發現用30作為輸入進行計算的時候,程式輸出結果時已經有了一定的時間延遲,而用再大的資料來測試就會發現結果在短時間內根本出不來,這是為什麼呢?
對於上面的遞迴求解方法來說,時間複雜度為O(2^n),所以效率非常慢,為了計算一個f(n),需要存在一個f(n-1)和一個f(n-2),然而f(n-1)遞迴地對f(n-2)h和f(n-3)進行呼叫,因此存在兩個單獨計算f(n-2)的呼叫。繼續跟蹤整個演算法會發現,f(n-3)被計算了3次,f(n-4)被計算了5次,而f(n-5)則是8次。冗餘的計算無疑加重了編譯器的負擔,如果編譯器不能對之前計算過的資料進行保留,這樣的增長就無法比避免。
所以這裡給出第二種演算法,時間複雜度為O(n)
public class Main {
public static int f (int n) {
if (n <= 2) {
return 1;
}
int last = 1;
int nextToLast = 1;
int answer = 1;
//前2位都為1
for (int i = 3; i <= n; i++) {
answer = last + nextToLast;
nextToLast = last;
last = answer;
}
return answer;
}
public static void main(String[] args) {
int n = 12;
System.out.println(f(n));
}
}
第二種演算法即使用非常大的資料進行測試也僅僅是稍有延遲,短時間內基本可以輸出。
下面我們來分析一下這兩種演算法的思想
分治策略是對於一個規模為n的問題,若該問題可以容易地解決(比如說規模n較小)則直接解決,否則將其分解為k個規模較小的子問題,這些子問題互相獨立且與原問題形式相同,遞迴地解這些子問題,然後將各子問題的解合併得到原問題的解。
提到分治策略就不得不提遞迴,遞迴與分治的關係網上說的有諸多含糊不清,愚見,分治是一種思想,而遞迴是實現分治思想的方法。
動態規劃的基本思想與分治策略類似,區別就在於動態規劃是一種帶基於記憶化搜素的思想,這也是不同於普通搜尋演算法的一大特點。
所以,動態規劃思想常用於解決問題中含有較多重疊子的問題,這種問題應儘量避免使用單純的遞迴演算法實現。