[Java 8] Lambda運算式對遞迴的最佳化(上),lambda運算式

來源:互聯網
上載者:User

[Java 8] Lambda運算式對遞迴的最佳化(上),lambda運算式
遞迴最佳化

很多演算法都依賴於遞迴,典型的比如分治法(Divide-and-Conquer)。但是普通的遞迴演算法在處理規模較大的問題時,常常會出現StackOverflowError。處理這個問題,我們可以使用一種叫做尾調用(Tail-Call Optimization)的技術來對遞迴進行最佳化。同時,還可以通過暫存子問題的結果來避免對子問題的重複求解,這個最佳化方法叫做備忘錄(Memoization)。

本文首先對尾遞迴進行介紹,下一票文章中會對備忘錄模式進行介紹。

使用尾調用最佳化

當遞迴演算法應用於大規模的問題時,容易出現StackOverflowError,這是因為需要求解的子問題過多,遞迴嵌套層次過深。這時,可以採用尾調用最佳化來避免這一問題。該技術之所以被稱為尾調用,是因為在一個遞迴方法中,最後一個語句才是遞迴調用。這一點和常規的遞迴方法不同,常規的遞迴通常發生在方法的中部,在遞迴結束返回了結果後,往往還會對該結果進行某種處理。

Java在編譯器層級並不支援尾遞迴技術。但是我們可以藉助Lambda運算式來實現它。下面我們會通過在階乘演算法中應用這一技術來實現遞迴的最佳化。以下代碼是沒有最佳化過的階乘遞迴演算法:

public class Factorial {    public static int factorialRec(final int number) {        if(number == 1)            return number;        else            return number * factorialRec(number - 1);    }}

以上的遞迴演算法在處理小規模的輸入時,還能夠正常求解,但是輸入大規模的輸入後就很有可能拋出StackOverflowError:

try {    System.out.println(factorialRec(20000));} catch(StackOverflowError ex) {    System.out.println(ex);}// java.lang.StackOverflowError

出現這個問題的原因不在於遞迴本身,而在於在等待遞迴調用結束的同時,還需要儲存了一個number變數。因為遞迴方法的最後一個操作是乘法操作,當求解一個子問題時(factorialRec(number - 1)),需要儲存當前的number值。所以隨著問題規模的增加,子問題的數量也隨之增多,每個子問題對應著調用棧的一層,當調用棧的規模大於JVM設定的閾值時,就發生了StackOverflowError。

轉換成尾遞迴

轉換成尾遞迴的關鍵,就是要保證對自身的遞迴調用是最後一個操作。不能像上面的遞迴方法那樣:最後一個操作是乘法操作。而為了避免這一點,我們可以先進行乘法操作,將結果作為一個參數傳入到遞迴方法中。但是僅僅這樣仍然是不夠的,因為每次發生遞迴調用時還是會在調用棧中建立一個棧幀(Stack Frame)。隨著遞迴調用深度的增加,棧幀的數量也隨之增加,最終導致StackOverflowError。可以通過將遞迴調用延遲化來避免棧幀的建立,以下代碼是一個原型實現:

public static TailCall<Integer> factorialTailRec(    final int factorial, final int number) {    if (number == 1)        return TailCalls.done(factorial);    else        return TailCalls.call(() -> factorialTailRec(factorial * number, number - 1));}

需要接受的參數factorial是初始值,而number是需要計算階乘的值。 我們可以發現,遞迴調用體現在了call方法接受的Lambda運算式中。以上代碼中的TailCall介面和TailCalls工具類目前還沒有實現。

建立TailCall函數介面

TailCall的目標是為了替代傳統遞迴中的棧幀,通過Lambda運算式來表示多個連續的遞迴調用。所以我們需要通過當前的遞迴操作得到下一個遞迴操作,這一點有些類似UnaryOperator函數介面的apply方法。同時,我們還需要方法來完成這幾個任務:

  1. 判斷遞迴是否結束了
  2. 得到最後的結果
  3. 觸發遞迴

因此,我們可以這樣設計TailCall函數介面:

@FunctionalInterfacepublic interface TailCall<T> {    TailCall<T> 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個任務。只不過具體的isComplete和result還需要根據遞迴操作的性質進行覆蓋,比如對於遞迴的中間步驟,isComplete方法可以返回false,然而對於遞迴的最後一個步驟則需要返回true。對於result方法,遞迴的中間步驟可以拋出異常,而遞迴的最終步驟則需要給出結果。

invoke方法則是最重要的一個方法,它會將所有的遞迴操作通過apply方法串聯起來,通過沒有棧幀的尾調用得到最後的結果。串聯的方式利用了Stream類型提供的iterate方法,它本質上是一個無窮列表,這也從某種程度上符合了遞迴調用的特點,因為遞迴調用發生的數量雖然是有限的,但是這個數量也可以是未知的。而給這個無窮列表畫上終止符的操作就是filter和findFirst方法。因為在所有的遞迴調用中,只有最後一個遞迴調用會在isComplete中返回true,當它被調用時,也就意味著整個遞迴調用鏈的結束。最後,通過findFirst來返回這個值。

如果不熟悉Stream的iterate方法,可以參考上一篇文章,在其中對該方法的使用進行了介紹。

建立TailCalls工具類

在原型設計中,會調用TailCalls工具類的call和done方法:

  • call方法用來得到當前遞迴的下一個遞迴
  • done方法用來結束一系列的遞迴操作,得到最終的結果
public class TailCalls {    public static <T> TailCall<T> call(final TailCall<T> nextCall) {        return nextCall;    }    public static <T> TailCall<T> done(final T value) {        return new TailCall<T>() {            @Override public boolean isComplete() { return true; }            @Override public T result() { return value; }            @Override public TailCall<T> apply() {                throw new Error("end of recursion");            }        };    }}

在done方法中,我們返回了一個特殊的TailCall執行個體,用來代表最終的結果。注意到它的apply方法被實現成被調用拋出異常,因為對於最終的遞迴結果,是沒有後續的遞迴操作的。

以上的TailCall和TailCalls雖然是為瞭解決階乘這一簡單的遞迴演算法而設計的,但是它們無疑在任何需要尾遞迴的演算法中都能夠派上用場。

使用尾遞迴函式

使用它們來解決階乘問題的代碼很簡單:

System.out.println(factorialTailRec(1, 5).invoke());      // 120System.out.println(factorialTailRec(1, 20000).invoke());  // 0

第一個參數代表的是初始值,第二個參數代表的是需要計算階乘的值。

但是在計算20000的階乘時得到了錯誤的結果,這是因為整型資料無法容納這麼大的結果,發生了溢出。對於這種情況,可以使用BigInteger來代替Integer類型。

實際上factorialTailRec的第一個參數是沒有必要的,在一般情況下初始值都應該是1。所以我們可以做出相應地簡化:

public static int factorial(final int number) {    return factorialTailRec(1, number).invoke();}// 調用方式System.out.println(factorial(5));System.out.println(factorial(20000));
使用BigInteger代替Integer

主要就是需要定義decrement和multiple方法來協助完成大整型資料的階乘操作:

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<BigInteger> 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();    }}

用java 8裡面的lambda運算式寫一個簡單加法運算

/*
一個介面,如果只有一個顯式聲明的抽象方法,
那麼它就是一個函數介面。
一般用@FunctionalInterface標註出來(也可以不標)
*/
public interface Inteface1{
//可以不用abstract修飾
public abstract void test(int x,int y);
//public void test1();//會報錯,不能有兩個方法,儘管沒有使用abstract修飾
public boolean equals(Object o);//equals屬於Object的方法,所以不會報錯
}

public class Test{
public static void main(String args[]){
Inteface1 f1=(int x,int y)->{System.out.println(x+y);};
f1.test(3,4);

Inteface1 f2=(int x,int y)->{ System.out.println("Hello Lambda!\t the result is " +(x+y));};
f2.test(3,4);
}
}
 
java8 lambda運算式

整個大概可以
return userResDao .findEqualByProperty("user.token", token) .parallelStream() .map(UserRes::getRes) .collect(Collectors.toList());但沒有調試環境,不保證細節

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.