[CS101] 轉載:淺議Fibonacci(斐波納契)數列求解

原文轉載自林健隨筆的「淺議Fibonacci(斐波納契)數列求解算法

Fibonacci 數列

fibonacci-algorithm-f1.gif

描述了動物繁殖數量、植物花序變化等天然規律。做爲一個經典的數學問題,Fibonacci數列常做爲例子出如今程序設計、數據結構與算法等多個相關學科中。編程

下面簡單地分析一下常見的Fibonacci數列求解算法。數據結構

遞歸法

大多數教材在講解遞歸算法時總喜歡以Fibonacci數列爲例,這是由於咱們能夠直觀地從定義公式的第三行看出Fibonacci數列的遞歸性。其C++實現以下:函數

unsigned long Fib(int n)
{
    if (n <= 1) {
        return n;
    } else {
        return Fib(n - 1) + Fib(n - 2);
    }
}

遞歸算法與定義公式十分吻合,容易理解,但計算過程存在大量重複的運算,時間複雜度達到了O(2^n),使用的內存空間也隨着函數調用棧的增加而增加。這顯然不適於實用的程序。測試

表驅動的遞歸法

(編者注:動態規劃)
這裏不提純粹的表驅動法,由於對於項數未知的Fibonacci數列開啓大片的空間來換取時間未免不值得且不負責。咱們只是爲了消除遞歸法中大量重複的運算,能夠將已經計算過的中間值存入一個表,已備後續使用:優化

#define MAX_LOG 20
static unsigned long Logs[MAX_LOG] = {0};
unsigned long Fib(int n)
{
    if (n <= 1) {
        return n;
    } else if (n < MAX_LOG && Logs[n] != 0) {
        return Logs[n];
    } else {
        Logs[n] = Fib(n - 1) + Fib(n - 2);
        return Logs[n];
    }
}

當n小於保存的表長時,因爲每一箇中間值只計算一次,時間複雜度降爲O(n)。但隨着n的增大,要想維持O(n)的時間複雜度,就必須擴大保存的表長,這就形成了存儲空間的浪費。ui

迭代法

(編者注:遞推)
求Fibonacci數列第n項時雖然要用到前面兩項的值,但它們僅做爲臨時計算的中間值,不做爲結果輸出,所以無保留的必要,徹底能夠轉化成迭代法求解:spa

unsigned long Fib(int n)
{
    int i;
    unsigned long a = 0, b = 1, c;
    if (n <= 1) {
        return n;
    } else {
        for (i = 2; i <= n; i++) {
            c = a + b;
            a = b;
            b = c;
        }
        return c;
    }
}

迭代法的時間複雜度爲O(n),使用的內存空間也不會動態上漲。我的認爲Fibonacci數列更適宜做爲迭代法而非遞歸法的典例出如今教材上。設計

下面給出兩種數學性較強的算法。考慮到表達的簡潔性,用Matlab實現指針

轉移矩陣法

此方法一般見於線性代數中的Markov過程示例。Fibonacci數列第n項與第n-1項能夠經過轉移矩陣的n-1次迭代求出:
fibonacci-algorithm-f2.gif

function y = Fib(n)
    first = [1; 0];
    trans = [1 1; 1 0];
    last = trans ^ (n - 1) * first;
    y = last(1, 1);
end

此算法的時間複雜度至關於計算矩陣乘方的時間複雜度。在計算2階矩陣n次方時,若是直接按矩陣乘法定義式展開,不加特別優化,其時間複雜度爲O(n)。

通項公式法

Fibonacci數列的通項公式以下(證實略):
fibonacci-algorithm-f3.gif

function y = Fib(n)
    sr5 = sqrt(5);
    y = uint32((((1 + sr5) / 2) ^ n - ((1 - sr5) / 2) ^ n) / sr5);
end

該方法的時間複雜度貌似爲O(1),但咱們還應該考慮乘方運算的時間消耗。不加特別優化時,用乘法實現n次乘方的時間複雜度爲O(n)。考慮到浮點數計算效率和精度問題,此方法在計算機實現時不如轉移矩陣法。

下面再給出兩種對計算機實現有特別意義,但同時有必定侷限性的實現方法(只是實現方法,不能稱爲新的算法)。其中使用了一些C++編程技巧,對使用其它語言實現也有必定的參考價值:

模板元編程法

一般咱們在C++中使用模板,僅限於類模板與函數模板。事實上C++支持模板元編程,理論上能夠在編譯時執行任何計算(甚至包含選擇、循環、遞歸等結構)。代碼以下:

#define Fib(N) FibT<N>::Val
template<int n> struct FibT
{
    enum
    {
        Val = FibT<n - 1>::Val + FibT<n - 2>::Val
    };
};
template<> struct FibT<0>
{
    enum
    {
        Val = 0
    };
};
template<> struct FibT<1>
{
    enum
    {
        Val = 1
    };
};

咱們用一個結構體做爲模板的載體,用一個枚舉值保存運算結果。其中第一個模板爲基本遞歸過程(使用遞歸算法是爲了說明的簡便,徹底能夠用其它算法替代,以加速編譯過程),後兩個模板爲n=0、1時的模板特化。經過#define語句將模板調用簡寫成相似函數調用的方式。程序在編譯時運算所需的 Fibonacci數列項,將結果做爲常量嵌入編譯好的程序。運行時直接使用結果,時間複雜度真正地變成了O(1)。但這一方法最大的侷限就是隻能對常量嵌入,程序中出現諸如計算Fib(i++)的狀況則無能爲力。儘管如此,這比在代碼中手工計算並寫入所需的值要直觀準確,比經過純粹的表驅動法「空間換時間」要方便快捷

函數對象法

此方法主要用於C++ STL編程的通用算法方面,其執行行爲也有別於以上其它方法:

class Fib
{
public:
    Fib() : a(0), b(1), n(0)
    {
    }
    unsigned long operator()()
    {
        if (n <= 1) {
            n++;
            return n - 1;
        } else {
            int c;
            c = a + b;
            a = b;
            b = c;
            return c;
        }
    }
private:
    int a, b, n;
};

這個函數類對象的行爲能夠理解爲一個「Fibonacci數列發生器」,其測試性調用以下,程序將依次打印

void test(int i)
{
    Fib fib;
    do {
        cout << fib() << endl;
    } while (i--);
}

函數對象具備與函數指針相似的行爲,同時又能保存自身的一些屬性,所以經常使用於STL通用算法編程。但針對單個的Fibonacci數列項求值,靈活性不如通常的方法。
但願讀者可以從上面的算法分析中觸類旁通,有所領悟。

參考資料

  1. Bruce Eckel,Thinking In C++ Volume 2: Practical Programming,機械工業出版社,2006

  2. William J. Collins,Data Structures and the Standard Template Library,機械工業出版社,2003

  3. Knott's Surrey University,The Home page for Fibonacci Numbers and the Golden Section,http://www.mcs.surrey.ac.uk/Personal/R.Knott/Fibonacci/

相關文章
相關標籤/搜索