兩個小例子輕鬆搞懂 java 中遞迴與尾遞迴的優化操作
廢話不多說,我們直接上兩個最常見的小例子:
一、遞迴,偽遞迴,迭代實現n!
package com.njbdqn.test02; /** * 遞迴,偽遞迴,迭代實現n! */ public class RecursionTest { public static void main(String[] args) { System.out.println(recurse(5)); //遞迴顯示 System.out.println(camouflageRecurse(5,1)); //偽遞迴 System.out.println(iteration(5)); //迭代 } /** * n的階乘,尾遞迴實現方式 * * @param n * @param result 計算儲存的中間結果 * @return 最終結果 */ public static int camouflageRecurse(int n,int result) { if (n == 1) { return result; } else { result = result * n; return camouflageRecurse(n - 1,result); } } /** * 求 n 的階乘遞迴呼叫方式 * * @param n n個數的階乘 * @return n個數階乘的結果 */ public static int recurse(int n) { if (n == 1) { return 1; } else { return n * recurse(n - 1); } } /** * 用迭代的方法實現n的階乘 * * @param n * @return */ public static int iteration(int n) { int result = 1; for (int i = 2; i <= n; ++i) { result *= i; } return result; } }
二、斐波那契數列的遞迴和迭代實現求和
package com.njbdqn.test02; /** * 斐波那契數列的遞迴和迭代實現求和 * 0 1 1 2 3 5 8 13 21 34 55 89 */ public class FibonacciTest { public static void main(String[] args) { System.out.println(fibonacciRecurse(14)); System.out.println(fibonacciIteration(14)); System.out.println(camouflageFibonacci(14,1,0)); } /** * 遞迴呼叫實現斐波那契數列 * * @param n * @return */ public static int fibonacciRecurse(int n) { if (n == 1) { return 0; } else if (n == 2) { return 1; } else { return fibonacciRecurse(n - 1) + fibonacciRecurse(n - 2); } } /** * 迭代實現斐波那契數列 * 0 1 1 2 3 5 8 13 21 34 55 89 * * @param n * @return */ public static int fibonacciIteration(int n) { int fab = 0; //最終結果 n的值 int pre = 1; //記錄n-1值 int p = 0; //記錄n-2的位置 if (n == 1) { fab = 0; } else if (n == 2) { fab = 1; } for (int i = 2; i < n; ++i) { fab = pre + p; p = pre; pre = fab; } return fab; } /** * 斐波那契數列尾遞迴實現 * 0 1 1 2 3 5 8 13 21 34 55 89 * * @param n * @return */ public static int camouflageFibonacci(int n,int result1,int result2) { if (n == 0) { return result1; } else { return camouflageFibonacci(n - 1,result2,result1+result2) ; } } }
上述兩個小例子我們都採用了迭代、遞迴和尾遞迴的方法去實現。迭代不必說,就是用我們java基礎的 for 迴圈去實現。而在遞迴和尾遞迴實際上都是java 基礎 oop 的自己呼叫自己方法的實現。尾遞迴實際上是對遞迴的優化。
遞迴
遞迴的本質是,某個方法中呼叫了自身。本質還是呼叫一個方法,只是這個方法正好是自身而已。
如第二個例子斐波那契數列的遞迴return fibonacciRecurse(n - 1) + fibonacciRecurse(n - 2)部分執行示意圖如下所示:
遞迴的三大特性:
呼叫的是同一個方法
因為呼叫的是同一個方法,所以只需要寫一個方法,就可以讓你輕鬆呼叫無數次,所以呼叫的方法數可能非常巨大,其實在實際問題中往往都是方法數呼叫巨大的情況。
在自身中呼叫自身,本身就是巢狀呼叫(棧幀無法回收,開銷巨大)
遞迴的侷限性:
因為遞迴呼叫的方法數大都非常巨大和巢狀呼叫帶來的棧幀無法回收,所以遞迴呼叫最大的詬病就是開銷巨大,棧幀和堆一起爆掉,俗稱記憶體溢位洩露。
java為了優化遞迴帶來的記憶體溢位洩露,就有了尾遞迴的誕生。那麼尾遞迴是如何優化遞迴的呢?
尾遞迴
尾遞迴優化是利用上面的第一個特點 “呼叫同一個方法” 來進行優化的。為了解決遞迴的開銷大問題,使用尾遞迴優化,具體分兩種方法:
尾遞迴優化方式:
尾遞迴的形式:把遞迴呼叫的形式寫成尾遞迴的形式
編譯器對尾遞迴的優化:編譯器碰到尾遞迴,自動按照某種特定的方式進行優化編譯
尾遞迴的形式:
尾遞迴其實只是一種對遞迴的特殊寫法,這種寫法原本並不會帶來跟遞迴不一樣的影響,它只是寫法不一樣而已,寫成這樣不會有任何優化效果,該爆的棧和幀都還會爆
遞迴的本質是某個方法呼叫了自身,尾遞迴這種形式就要求:某個方法呼叫自身這件事,一定是該方法做的最後一件事(所以當有需要返回值的時候會是return f(n),沒有返回的話就直接是f(n)了)
這個f(n)外不能加其他東西,因為這就不是最後一件事了,值返回來後還要再幹點其他的活,變數空間還需要保留。比如如果有返回值的,你不能:乘個常數 return 3f(n);乘個n return n*f(n);甚至是 f(n)+f(n-1)…
另外,使用return的尾遞迴還跟函數語言程式設計有一點關係
編譯器對尾遞迴的優化
簡單說就是重複利用同一個棧幀,不僅不用釋放上一個,連下一個新的都不用開,效率非常高
一方面是因為在遞迴呼叫自身的時候,這一層函式已經沒有要做的事情了,雖然被遞迴呼叫的函式是在當前的函式裡,但是他們之間的關係已經在傳參的時候了斷了,也就是這一層函式的所有變數什麼的都不會再被用到了,所以當前函式雖然沒有執行完,不能彈出棧,但它確實已經可以出棧了
另一方面是正因為呼叫的是自身,所以需要的儲存空間是一毛一樣的,那乾脆重新重新整理這些空間給下一層利用就好了,不用銷燬再另開空間
如第二個例子斐波那契數列的尾遞迴return camouflageFibonacci(n - 1,result1+result2)部分執行示意圖如下所示:
說到這裡你很容易聯想到JAVA中的自動垃圾回收機制,同是處理記憶體問題的機制,尾遞迴優化跟垃圾回收是不是有什麼關係,這是不是就是JAVA不實現尾遞迴優化的原因?
垃圾回收(GC)與 尾遞迴
首先我們需要談一下記憶體機制,這裡我們需要了解記憶體機制的兩個部分:棧和堆。
在Java中, JVM中的棧記錄了執行緒的方法呼叫。每個執行緒擁有一個棧。在某個執行緒的執行過程中, 如果有新的方法呼叫,那麼該執行緒對應的棧就會增加一個儲存單元,即棧幀 (frame)。在frame 中,儲存有該方法呼叫的引數、區域性變數和返回地址。Java的引數和區域性變數只能是 基本型別 的變數(比如 int),或者物件的引用(reference) 。因此,在棧中,只儲存有基本型別的變數和物件引用。而引用所指向的物件儲存在堆中。具體如下圖所示:
當被呼叫方法執行結束時,該方法對應的幀將被刪除,引數和區域性變數所佔據的空間也隨之釋放。執行緒回到原方法,繼續執行。當所有的棧都清空時,程式也隨之執行結束。如上所述,棧 (stack)可以自己照顧自己。但堆必須要小心對待。堆是 JVM中一塊可自由分配給物件的區域。當我們談論垃圾回收 (garbage collection) 時,我們主要回收堆(heap)的空間。Java的普通物件存活在堆中。與棧不同,堆的空間不會隨著方法呼叫結束而清空(即使它在棧上的引用已經被清空了)(也不知道為什麼不直接同步清空)。因此,在某個方法中建立的物件,可以在方法呼叫結束之後,繼續存在於堆中。這帶來的一個問題是,如果我們不斷的建立新的物件,記憶體空間將最終消耗殆盡。如果沒有垃圾回收機制的話,你就需要手動地顯式分配及釋放記憶體,如果你忘了去釋放記憶體,那麼這塊記憶體就無法重用了(不管是什麼區域性變數還是其他的什麼)。這塊記憶體被佔有了卻沒被使用,這種場景被稱之為記憶體洩露。
如下圖所示:第二個例子斐波那契數列的尾遞迴每次呼叫自己的方法相當於在記憶體中快取一個Object 的camouflageFibonacci 方法物件的引用,不會去釋放,直到程式結束。
最原始的情況,都是需要手動釋放堆中的物件,所以你經常需要考慮物件的生存週期,但是JAVA則引入了一個自動垃圾回收的機制,它能智慧地釋放那些被判定已經沒有用的物件。
尾遞迴優化和垃圾回收最本質的區別是,尾遞迴優化解決的是記憶體溢位的問題,而垃圾回收解決的是記憶體洩露的問題。
記憶體洩露:指程式中動態分配記憶體給一些臨時物件,但是物件不會被GC所回收,它始終佔用記憶體。即被分配的物件可達但已無用。
記憶體溢位:指程式執行過程中無法申請到足夠的記憶體而導致的一種錯誤。記憶體溢位通常發生於OLD段或Perm段垃圾回收後,仍然無記憶體空間容納新的Java物件的情況。
從定義上可以看出記憶體洩露是記憶體溢位的一種誘因,不是唯一因素。
自動垃圾回收機制的特點是:
解決了所有情況下的記憶體洩露的問題,但還可以由於其他原因記憶體溢位
針對記憶體中的堆空間
正在執行的方法中的堆中的物件是不會被管理的,因為還有引用(棧幀沒有被清空)
一般簡單的自動垃圾回收機制是採用 引用計數 (reference counting)的機制。每個物件包含一個計數器。當有新的指向該物件的引用時,計數器加 1。當引用移除時,計數器減 1,當計數器為0時,認為該物件可以進行垃圾回收
與之相對,尾遞迴優化的特點是:
優化了遞迴呼叫時的記憶體溢位問題
針對記憶體中的堆空間和棧空間
只在遞迴呼叫的時候使用,而且只能對於寫成尾遞迴形式的遞迴進行優化
正在執行的方法的堆和棧空間正是優化的目標
以上這篇兩個小例子輕鬆搞懂 java 中遞迴與尾遞迴的優化操作就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。