什麼叫遞歸?舉個例子,咱們排隊,想知道本身排在第幾個,那麼咱們能夠問前面的那我的,前面的人繼續問前面,直到問到第一我的,這就是傳遞的過程。而後再從第一我的回來,這就是歸(回溯)的過程。傳遞過去再回歸回來,這就是遞歸。第一我的就是咱們所說的遞歸出口,也就是說到哪一個點應該回歸了,若是沒有出口,那麼就會死循環了棧溢出。數組
在代碼中簡單來講就是本身調用本身。拿到本身的結果再做爲入參調用本身。緩存
好比咱們求階乘:函數
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); }
調用過程以下:spa
走到1的時候最後再一次回來計算結果,最後返回。這就是遞歸。3d
再來看一個比較經典的例子,裴波那契數列,1,1,2,3,5,8,13,21....後一項永遠是前兩項之和,code
用遞歸來實現:blog
/** * 裴波那契數列 遞歸 * @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,你會發現好久都計算不出來,爲何呢?遞歸
遞歸一個遞和一個歸的過程無疑是增長了時間複雜度的,階乘那個的時間複雜度還好O(2n)也就是O(n),可是裴波那契數列就是O(2^n),由於每一個值進去都有兩個分支,就像1生2,2生4,4生8這種了,因此是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); }
因此咱們對於遞歸的使用必定要慎重!!!