算法---遞歸及尾遞歸

什麼叫遞歸?舉個例子,我們排隊,想知道自己排在第幾個,那麼我們可以問前面的那個人,前面的人繼續問前面,直到問到第一個人,這就是傳遞的過程。然後再從第一個人回來,這就是歸(回溯)的過程。傳遞過去再回歸回來,這就是遞歸。第一個人就是我們所說的遞歸出口,也就是說到哪個點應該回歸了,如果沒有出口,那麼就會死循環了棧溢出。

 

在代碼中簡單來說就是自己調用自己。拿到自己的結果再作爲入參調用自己。

比如我們求階乘:

5的階乘: 5*4*3*2*1

我們用遞歸來寫,那麼出口就是n=1的時候:代碼如下

    /**
     * 階乘
     * @param n
     * @return
     */
    public static int factorial(int n) {
        if (n <= 1) {
            return 1;
        }
        return n * factorial(n - 1);
    }

調用過程如下:

 

 走到1的時候最後再一次回來計算結果,最後返回。這就是遞歸。

 

再來看一個比較經典的例子,裴波那契數列,1,1,2,3,5,8,13,21....後一項永遠是前兩項之和,

 

用遞歸來實現:

 

    /**
     * 裴波那契數列 遞歸
     * @param n
     * @return
     */
    public static int recursion(int n) {
        if (n <= 2) {
            return n == 0 ? 0 : 1;
        }
        return recursion(n - 1) + recursion(n - 2);
    }

 

是不是感覺遞歸寫起來代碼很少。看起來也乾淨。但是當執行上面裴波那契數列的代碼,比如設置一個45,你會發現很久都計算不出來,爲什麼呢?

遞歸一個遞和一個歸的過程無疑是增加了時間複雜度的,階乘那個的時間複雜度還好O2n)也就是On),但是裴波那契數列就是O2^n因爲每個值進去都有兩個分支,就像12,24,48這種了,所以是2^n。就像這樣:

 

 從這個圖裏面還看出來幾乎所有的值都會被多次計算,在每一個分支都去計算多次。

所以我們需要來優化我們上面的代碼:

1.非遞歸實現,按理來說,每一個遞歸都可以用非遞歸來實現

裴波那契數列非遞歸實現,時間複雜度O(n)

    /**
     * 裴波那契數列循環實現
     * @param n
     * @return
     */
    public static int cycle(int n) {
        if (n <= 2) {
            return n <= 0 ? 0 : 1;
        }
        int f1 = 1; //n-1
        int f2 = 1; //n-2
        int fn = 0; // n
        for (int i =3; i<=n;i++) {
            fn = f1 + f2;
            f2 = f1;
            f1 = fn ;
        }
        return fn ;
    }

 

2.保存中間結果,剛纔也說到了,裴波那契數列那個遞歸的實現,會讓很多值多次計算,聲明一個數組做緩存,把中間結果放入數組中存起來,計算的時候先去數組中取,如果有就不計算了

    public static int cache(int n) { 
        int data[] = new int[n]; // 用數組來做緩存
        return fac(n, data);
    }

    public static int fac(int n, int[] data) {
        if (n <= 2)
            return 1; //遞歸的終止條件
        if (data[n-1] > 0) {  //數組中有值就直接取出來返回,不用再去計算
            return data[n-1];
        }
        int res = fac(n - 1, data) + fac(n - 2, data);
        data[n-1] = res;  //算出來值放到數組中
        return res;
    }

 

3.尾遞歸爲什麼有些遞歸會棧溢出,因爲每個方法調用都會創建新的棧。如果沒有控制好遞歸的深度,肯定是會棧溢出的。

尾遞歸就是,函數調用在末尾,且末尾只能有函數調用,不能有其他操作。這樣編譯器在編譯代碼的時候如果發現末尾只有函數調用,不會創建新的棧。也就說最後我們的方法返回就是返回的我們的最終結果。如何才能做到這樣呢,其實就需要將前面的計算結果傳遞到最後,遞歸出口即是結束,沒有回溯的過程。

階乘的尾遞歸實現:

 

    /**
     * 階乘尾遞歸
     * @param n
     * @return
     */
    public static int taiFactorial(int n, int result) {
        if (n <= 1) {
            return result; //最後返回的即是最終結果
        }
        return taiFactorial(n - 1, n * result);//結果往下傳
    }

 

 裴波那契數列尾遞歸:

    /**
     * 尾遞歸 裴波那契
     * @param pre 上上一次運算出來的結果
     * @param result  上一次運算出來結果
     * @param n
     * @return
     */
    public static int tailRecursion(int pre, int result, int n) {
        if (n <= 2) {
            return result;
        }
        //對於下一次調用來說 前一次結果 pre + result  前前一次result
        return tailRecursion(result, pre + result, n - 1);
    }

 

測試下裴波那契數列普通遞歸和尾遞歸執行效率:

    public static void main(String[] args){
        System.out.println("---裴波那契數列普通遞歸---");
        long time1 = System.currentTimeMillis();
        int result = recursion(45);
        System.out.println(result);
        long time2 = System.currentTimeMillis();
        System.out.println(time2-time1);
        System.out.println("---裴波那契數列尾遞歸---");
        result = tailRecursion(1,1,45);
        System.out.println(result);
        long time3 = System.currentTimeMillis();
        System.out.println(time3-time2);

    }

 

 

所以我們對於遞歸的使用一定要慎重!!!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章