foreach jdk8 递归_[Java 8] (8) Lambda表达式对递归的优化(上) - 使用尾递归 .
遞歸優(yōu)化
很多算法都依賴于遞歸,典型的比如分治法(Divide-and-Conquer)。但是普通的遞歸算法在處理規(guī)模較大的問題時,常常會出現(xiàn)StackOverflowError。處理這個問題,我們可以使用一種叫做尾調(diào)用(Tail-Call Optimization)的技術(shù)來對遞歸進行優(yōu)化。同時,還可以通過暫存子問題的結(jié)果來避免對子問題的重復求解,這個優(yōu)化方法叫做備忘錄(Memoization)。
本文首先對尾遞歸進行介紹,下一票文章中會對備忘錄模式進行介紹。
使用尾調(diào)用優(yōu)化
當遞歸算法應(yīng)用于大規(guī)模的問題時,容易出現(xiàn)StackOverflowError,這是因為需要求解的子問題過多,遞歸嵌套層次過深。這時,可以采用尾調(diào)用優(yōu)化來避免這一問題。該技術(shù)之所以被稱為尾調(diào)用,是因為在一個遞歸方法中,最后一個語句才是遞歸調(diào)用。這一點和常規(guī)的遞歸方法不同,常規(guī)的遞歸通常發(fā)生在方法的中部,在遞歸結(jié)束返回了結(jié)果后,往往還會對該結(jié)果進行某種處理。
Java在編譯器級別并不支持尾遞歸技術(shù)。但是我們可以借助Lambda表達式來實現(xiàn)它。下面我們會通過在階乘算法中應(yīng)用這一技術(shù)來實現(xiàn)遞歸的優(yōu)化。以下代碼是沒有優(yōu)化過的階乘遞歸算法:
public class Factorial {
public static int factorialRec(final int number) {
if(number == 1)
return number;
else
return number * factorialRec(number - 1);
}
}
以上的遞歸算法在處理小規(guī)模的輸入時,還能夠正常求解,但是輸入大規(guī)模的輸入后就很有可能拋出StackOverflowError:
try {
System.out.println(factorialRec(20000));
} catch(StackOverflowError ex) {
System.out.println(ex);
}
// java.lang.StackOverflowError
出現(xiàn)這個問題的原因不在于遞歸本身,而在于在等待遞歸調(diào)用結(jié)束的同時,還需要保存了一個number變量。因為遞歸方法的最后一個操作是乘法操作,當求解一個子問題時(factorialRec(number - 1)),需要保存當前的number值。所以隨著問題規(guī)模的增加,子問題的數(shù)量也隨之增多,每個子問題對應(yīng)著調(diào)用棧的一層,當調(diào)用棧的規(guī)模大于JVM設(shè)置的閾值時,就發(fā)生了StackOverflowError。
轉(zhuǎn)換成尾遞歸
轉(zhuǎn)換成尾遞歸的關(guān)鍵,就是要保證對自身的遞歸調(diào)用是最后一個操作。不能像上面的遞歸方法那樣:最后一個操作是乘法操作。而為了避免這一點,我們可以先進行乘法操作,將結(jié)果作為一個參數(shù)傳入到遞歸方法中。但是僅僅這樣仍然是不夠的,因為每次發(fā)生遞歸調(diào)用時還是會在調(diào)用棧中創(chuàng)建一個棧幀(Stack Frame)。隨著遞歸調(diào)用深度的增加,棧幀的數(shù)量也隨之增加,最終導致StackOverflowError??梢酝ㄟ^將遞歸調(diào)用延遲化來避免棧幀的創(chuàng)建,以下代碼是一個原型實現(xiàn):
public static TailCall factorialTailRec(
final int factorial, final int number) {
if (number == 1)
return TailCalls.done(factorial);
else
return TailCalls.call(() -> factorialTailRec(factorial * number, number - 1));
}
需要接受的參數(shù)factorial是初始值,而number是需要計算階乘的值。 我們可以發(fā)現(xiàn),遞歸調(diào)用體現(xiàn)在了call方法接受的Lambda表達式中。以上代碼中的TailCall接口和TailCalls工具類目前還沒有實現(xiàn)。
創(chuàng)建TailCall函數(shù)接口
TailCall的目標是為了替代傳統(tǒng)遞歸中的棧幀,通過Lambda表達式來表示多個連續(xù)的遞歸調(diào)用。所以我們需要通過當前的遞歸操作得到下一個遞歸操作,這一點有些類似UnaryOperator函數(shù)接口的apply方法。同時,我們還需要方法來完成這幾個任務(wù):
判斷遞歸是否結(jié)束了
得到最后的結(jié)果
觸發(fā)遞歸
因此,我們可以這樣設(shè)計TailCall函數(shù)接口:
@FunctionalInterface
public interface TailCall {
TailCall apply();
default boolean isComplete() { return false; }
default T result() { throw new Error("not implemented"); }
default T invoke() {
return Stream.iterate(this, TailCall::apply)
.filter(TailCall::isComplete)
.findFirst()
.get()
.result();
}
}
isComplete,result和invoke方法分別完成了上述提到的3個任務(wù)。只不過具體的isComplete和result還需要根據(jù)遞歸操作的性質(zhì)進行覆蓋,比如對于遞歸的中間步驟,isComplete方法可以返回false,然而對于遞歸的最后一個步驟則需要返回true。對于result方法,遞歸的中間步驟可以拋出異常,而遞歸的最終步驟則需要給出結(jié)果。
invoke方法則是最重要的一個方法,它會將所有的遞歸操作通過apply方法串聯(lián)起來,通過沒有棧幀的尾調(diào)用得到最后的結(jié)果。串聯(lián)的方式利用了Stream類型提供的iterate方法,它本質(zhì)上是一個無窮列表,這也從某種程度上符合了遞歸調(diào)用的特點,因為遞歸調(diào)用發(fā)生的數(shù)量雖然是有限的,但是這個數(shù)量也可以是未知的。而給這個無窮列表畫上終止符的操作就是filter和findFirst方法。因為在所有的遞歸調(diào)用中,只有最后一個遞歸調(diào)用會在isComplete中返回true,當它被調(diào)用時,也就意味著整個遞歸調(diào)用鏈的結(jié)束。最后,通過findFirst來返回這個值。
如果不熟悉Stream的iterate方法,可以參考上一篇文章,在其中對該方法的使用進行了介紹。
創(chuàng)建TailCalls工具類
在原型設(shè)計中,會調(diào)用TailCalls工具類的call和done方法:
call方法用來得到當前遞歸的下一個遞歸
done方法用來結(jié)束一系列的遞歸操作,得到最終的結(jié)果
public class TailCalls {
public static TailCall call(final TailCall nextCall) {
return nextCall;
}
public static TailCall done(final T value) {
return new TailCall() {
@Override public boolean isComplete() { return true; }
@Override public T result() { return value; }
@Override public TailCall apply() {
throw new Error("end of recursion");
}
};
}
}
在done方法中,我們返回了一個特殊的TailCall實例,用來代表最終的結(jié)果。注意到它的apply方法被實現(xiàn)成被調(diào)用拋出異常,因為對于最終的遞歸結(jié)果,是沒有后續(xù)的遞歸操作的。
以上的TailCall和TailCalls雖然是為了解決階乘這一簡單的遞歸算法而設(shè)計的,但是它們無疑在任何需要尾遞歸的算法中都能夠派上用場。
使用尾遞歸函數(shù)
使用它們來解決階乘問題的代碼很簡單:
System.out.println(factorialTailRec(1, 5).invoke()); // 120
System.out.println(factorialTailRec(1, 20000).invoke()); // 0
第一個參數(shù)代表的是初始值,第二個參數(shù)代表的是需要計算階乘的值。
但是在計算20000的階乘時得到了錯誤的結(jié)果,這是因為整型數(shù)據(jù)無法容納這么大的結(jié)果,發(fā)生了溢出。對于這種情況,可以使用BigInteger來代替Integer類型。
實際上factorialTailRec的第一個參數(shù)是沒有必要的,在一般情況下初始值都應(yīng)該是1。所以我們可以做出相應(yīng)地簡化:
public static int factorial(final int number) {
return factorialTailRec(1, number).invoke();
}
// 調(diào)用方式
System.out.println(factorial(5));
System.out.println(factorial(20000));
使用BigInteger代替Integer
主要就是需要定義decrement和multiple方法來幫助完成大整型數(shù)據(jù)的階乘操作:
public class BigFactorial {
public static BigInteger decrement(final BigInteger number) {
return number.subtract(BigInteger.ONE);
}
public static BigInteger multiply(
final BigInteger first, final BigInteger second) {
return first.multiply(second);
}
final static BigInteger ONE = BigInteger.ONE;
final static BigInteger FIVE = new BigInteger("5");
final static BigInteger TWENTYK = new BigInteger("20000");
//...
private static TailCall factorialTailRec(
final BigInteger factorial, final BigInteger number) {
if(number.equals(BigInteger.ONE))
return done(factorial);
else
return call(() ->
factorialTailRec(multiply(factorial, number), decrement(number)));
}
public static BigInteger factorial(final BigInteger number) {
return factorialTailRec(BigInteger.ONE, number).invoke();
}
}
總結(jié)
以上是生活随笔為你收集整理的foreach jdk8 递归_[Java 8] (8) Lambda表达式对递归的优化(上) - 使用尾递归 .的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 魔兽世界怀旧服真银矿石怎么获得 真银矿石
- 下一篇: apache camel 相关配置_Ap