每天一道算法題——斐波那契數列

題目描述
大家都知道斐波那契數列,現在要求輸入一個整數n,請你輸出斐波那契數列的第n項。
n<=39
測試用例:
0 1 2 3 4 … 38
對應輸出應該爲:
0 1 1 2 … 39088169

1.使用簡單的遞歸:
源碼:

public class Test1 {
    public int Fibonacci(int n) {
        if(n<=1) {return n;}
        else {return Fibonacci(n-1)+Fibonacci(n-2);}
    }
}

但這樣肯定不行,因爲可能會導致Stack Overflow。
原因:

當計算序列爲4的斐波那契數的時候,
Fibonacci(4) = Fibonacci(3) + Fibonacci(2);
= Fibonacci(2) + Fibonacci(1) + Fibonacci(1) + Fibonacci(0);
= Fibonacci(1) + Fibonacci(0) + Fibonacci(1) + Fibonacci(1) + Fibonacci(0);
由於沒有記錄Fibonacci(1)和Fibonacci(0)的結果,對於程序來說它每次遞歸都要進行重複計算。所以僅僅計算Fibonacci(4)就需要分解爲三步。

對上述代碼進行適當的改進:
2.簡單的動態規劃
此方法算是屬於以空間換時間的做法了。
使用一個數組將所有的遞歸結果都記錄,到時只需返回數組中的值即可。時間複雜度到了O(n)

public class Test1 {
    public int Fibonacci(int n) {
        if (n <= 1) {
            return n;               //考慮負數,和F(0)F(1)的情況
        }
        int[] mark = new int[n + 1];//創建一個大小容納一個斐波那契數列的數組
        mark[0] = 0;                //將前兩位進行默認地初始化
        mark[1] = 1;
        for (int i = 2; i <= n; i++) {
            mark[i] = mark[i - 1] + mark[i - 2];//使用遞歸
        }
        return mark[n];             //傳回數組末尾的值
    }
}

那麼既然知道了浪費空間,是否可以避免這一點並進行優化呢?
3.使用循環:
此時,算法複雜度同樣達到了O(n),也避免了開闢一個不必要的數組

public class Test1 {
    public int Fibonacci(int n) {
        int Fn1 = 1;//相當於F(n-1)
        int Fn2 = 0;//相當於F(n-2)
        int result = 0;
        if (n <2)
            return n;
        for (int i = 2; i <= n; i++) {
            result = Fn1 + Fn2;
            Fn2 = Fn1;
            Fn1 = result;
        }
        return result;
    }
}

4.尾遞歸
存在着一種特殊的遞歸優化方法可以達到上面循環的功效:低空間複雜度,低時間複雜度。
原理:遞歸本質上是棧,可能導致棧溢出,只要避免溢出就可以了。

尾遞歸:如果一個函數中所有遞歸形式的調用都出現在函數的末尾,我們稱這個遞歸函數是尾遞歸的。當遞歸調用是整個函數體中最後執行的語句且它的返回值不屬於表達式的一部分時,這個遞歸調用就是尾遞歸。
特性:尾遞歸函數的特點是在迴歸過程中不用做任何操作。具體解釋請參考尾遞歸

只是Java中並沒有對尾遞歸做優化,但是當我們使用不那麼大的數字的時候,尾遞歸卻也表現出比第一種方法更好的運行測試結果。所以我們可以有如下代碼

源碼:

public class Test1 {

    public int Fibonacci(int n) {
        return Fibonacci(n, 0, 1);
    }

    private static int Fibonacci(int n, int acc1, int acc2) {
        if (n == 0)
            return 0;
        if (n == 1)
            return acc2;
        else
            return Fibonacci(n - 1, acc2, acc1 + acc2);
    }
}

5.矩陣快速冪方法:
線性代數中,存在如下等式:
有等式存在
等式可以簡化爲:
簡化後的等式

算法核心是:
* 1.矩陣的乘法
* 2.矩陣快速冪(因爲如果不用快速冪的算法,時間複雜度也只能達到O(N),而此算法複雜度爲O(logN))
* 此處只粘貼處源碼
* 具體的理解可以參考知乎大神的回答,很詳細易懂:王希的回答
源碼:

public class Test1 {

    public int Fibonacci(int n) {
        if (n < 1) {
        return 0;
        }
        if (n == 1 || n == 2) {
        return 1;
        }//底
        int[][] base = {{1,1},{1,0}};
        //求底爲base矩陣的n-2次冪
        int[][] res = matrixPower(base, n - 2);
        //根據[f(n),f(n-1)] = [1,1] * {[1,1],[1,0]}^(n-2),f(n)就是
        //1*res[0][0] + 1*res[1][0]
        return res[0][0] + res[1][0];
        }
    // 矩陣乘法
    public int[][] multiMatrix(int[][] m1, int[][] m2) {
        // 參數判斷什麼的就不給了,如果矩陣是n*m和m*p,那結果是n*p
        int[][] res = new int[m1.length][m2[0].length];
        for (int i = 0; i < m1.length; i++) {
            for (int j = 0; j < m2[0].length; j++) {
                for (int k = 0; k < m2.length; k++) {
                    res[i][j] += m1[i][k] * m2[k][j];
                }
            }
        }
        return res;
    }

    /*
     *      * 矩陣的快速冪:      * 1.假如不是矩陣,叫你求m^n,如何做到O(logn)?答案就是整數的快速冪:      *
     * 假如不會溢出,如10^75,把75用用二進制表示:1001011,那麼對應的就是:      * 10^75 =
     * 10^64*10^8*10^2*10      * 2.把整數換成矩陣,是一樣的      
     */
    public int[][] matrixPower(int[][] m, int p) {
        int[][] res = new int[m.length][m[0].length];
        // 先把res設爲單位矩陣
        for (int i = 0; i < res.length; i++) {
            res[i][i] = 1;
        } // 單位矩陣乘任意矩陣都爲原來的矩陣
            // 用來保存每次的平方
        int[][] tmp = m;
        // p每循環一次右移一位
        for (; p != 0; p >>= 1) {
            // 如果該位不爲零,應該乘
            if ((p & 1) != 0) {
                res = multiMatrix(res, tmp);
            }
            // 每次保存一下平方的結果
            tmp = multiMatrix(tmp, tmp);
        }
        return res;
    }
}

運行測試:

*第一種:
運行時間:1277ms
佔用內存:15256k
*第二種:
運行時間:20ms
佔用內存:15316k
*第三種:
運行時間:15ms
佔用內存:20780k
*第四種:
運行時間:24ms
佔用內存:15280k
*第五種:
運行時間:16ms
佔用內存:16784k

總結:
在Java中因爲並沒有對尾遞歸做優化,所以Java程序員在涉及遞歸的問題上時,一般使用循環而不是遞歸。
在這個問題上,給出n<=39,並沒有涉及很大的數,所以使用尾遞歸的測試結果居然出乎意料的好。
這幾種方法,簡單的遞歸最容易理解,矩陣快速冪複雜度最低。平時折中使用的話還是選擇循環或者動態規劃方法更好一些。
在針對尾遞歸做過優化的語言中也可選擇使用尾遞歸。簡單易懂且好用。

發佈了45 篇原創文章 · 獲贊 8 · 訪問量 9909
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章