1. 程式人生 > >斐波那契數列(一)--對比遞迴與動態規劃(JAVA)

斐波那契數列(一)--對比遞迴與動態規劃(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個規模較小的子問題,這些子問題互相獨立且與原問題形式相同,遞迴地解這些子問題,然後將各子問題的解合併得到原問題的解。
提到分治策略就不得不提遞迴,遞迴與分治的關係網上說的有諸多含糊不清,愚見,分治是一種思想,而遞迴是實現分治思想的方法

動態規劃的基本思想與分治策略類似,區別就在於動態規劃是一種帶基於記憶化搜素的思想,這也是不同於普通搜尋演算法的一大特點。
所以,動態規劃思想常用於解決問題中含有較多重疊子的問題,這種問題應儘量避免使用單純的遞迴演算法實現。