欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Java8使用lambda實(shí)現(xiàn)Java的尾遞歸

 更新時(shí)間:2017年10月26日 11:17:55   作者:Nemo  
這篇文章主要介紹了Java8使用lambda實(shí)現(xiàn)Java的尾遞歸的相關(guān)資料,需要的朋友可以參考下

前言

本篇介紹的不是什么新知識(shí),而是對(duì)前面講解的一些知識(shí)的綜合運(yùn)用。眾所周知,遞歸是解決復(fù)雜問(wèn)題的一個(gè)很有效的方式,也是函數(shù)式語(yǔ)言的核心,在一些函數(shù)式語(yǔ)言中,是沒(méi)有迭代與while這種概念的,因?yàn)榇祟惖难h(huán)通通可以用遞歸來(lái)實(shí)現(xiàn),這類語(yǔ)言的編譯器都對(duì)遞歸的尾遞歸形式進(jìn)行了優(yōu)化,而Java的編譯器并沒(méi)有這樣的優(yōu)化,本篇就要完成這樣一個(gè)對(duì)于尾遞歸的優(yōu)化。

什么是尾遞歸

本篇將使用遞歸中最簡(jiǎn)單的階乘計(jì)算來(lái)作為例子

遞歸實(shí)現(xiàn)

 /**
 * 階乘計(jì)算 -- 遞歸解決
 *
 * @param number 當(dāng)前階乘需要計(jì)算的數(shù)值
 * @return number!
 */
 public static int factorialRecursion(final int number) {
 if (number == 1) return number;
 else return number * factorialRecursion(number - 1);
 }

這種方法計(jì)算階乘比較大的數(shù)很容易就棧溢出了,原因是每次調(diào)用下一輪遞歸的時(shí)候在棧中都需要保存之前的變量,所以整個(gè)棧結(jié)構(gòu)類似是這樣的

5
  4
    3
      2
        1
------------------->

      棧的深度

在沒(méi)有遞歸到底之前,那些中間變量會(huì)一直保存著,因此每一次遞歸都需要開(kāi)辟一個(gè)新的棧空間

尾遞歸實(shí)現(xiàn)

任何遞歸的尾遞歸版本都十分簡(jiǎn)單,分析上面棧溢出的原因就是在每次return的時(shí)候都會(huì)附帶一個(gè)變量,因此只需要在return的時(shí)候不附帶這個(gè)變量即可。說(shuō)起來(lái)簡(jiǎn)單,該怎么做呢?其實(shí)也很容易,我們使用一個(gè)參數(shù)來(lái)保存上一輪遞歸的結(jié)果,這樣就可以了,因此尾遞歸的階乘實(shí)現(xiàn)應(yīng)該是這樣的代碼。

 /**
 * 階乘計(jì)算 -- 尾遞歸解決
 *
 * @param factorial 上一輪遞歸保存的值
 * @param number 當(dāng)前階乘需要計(jì)算的數(shù)值
 * @return number!
 */
 public static int factorialTailRecursion(final int factorial, final int number) {
 if (number == 1) return factorial;
 else return factorialTailRecursion(factorial * number, number - 1);
 }

使用一個(gè)factorial變量保存上一輪階乘計(jì)算出的數(shù)值,這樣return的時(shí)候就無(wú)需保存變量,整個(gè)的計(jì)算過(guò)程是
(5*4)20 -> (20*3) 60 -> (60*2) 120 -> return 120

這樣子通過(guò)每輪遞歸結(jié)束后刷新當(dāng)前的??臻g,復(fù)用了棧,就克服了遞歸的棧溢出問(wèn)題,像這樣的return后面不附帶任何變量的遞歸寫法,也就是遞歸發(fā)生在函數(shù)最尾部,我們稱之為'尾遞歸'。

使用lambda實(shí)現(xiàn)編譯器的優(yōu)化

很顯然,如果事情這么簡(jiǎn)單的話,這篇文章也就結(jié)束了,和lambda也沒(méi)啥關(guān)系 :) 然而當(dāng)你調(diào)用上文的尾遞歸寫法之后,發(fā)現(xiàn)并沒(méi)有什么作用,該棧溢出的還是會(huì)棧溢出,其實(shí)原因我在開(kāi)頭就已經(jīng)說(shuō)了,尾遞歸這樣的寫法本身并不會(huì)有什么用,依賴的是編譯器對(duì)尾遞歸寫法的優(yōu)化,在很多語(yǔ)言中編譯器都對(duì)尾遞歸有優(yōu)化,然而這些語(yǔ)言中并不包括java,因此在這里我們使用lambda的懶加載(惰性求值)機(jī)制來(lái)延遲遞歸的調(diào)用,從而實(shí)現(xiàn)棧幀的復(fù)用。

設(shè)計(jì)尾遞歸的接口

因此我們需要設(shè)計(jì)一個(gè)這樣的函數(shù)接口來(lái)代替遞歸中的棧幀,通過(guò)apply這個(gè)函數(shù)方法(取名叫apply是因?yàn)樵摲椒ǖ膮?shù)是一個(gè)棧幀,返回值也是一個(gè)棧幀,類比f(wàn)unction接口的apply)完成每個(gè)棧幀之間的連接,除此之外,我們棧幀還需要定義幾個(gè)方法來(lái)豐富這個(gè)尾遞歸的接口。

apply(連接棧幀,惰性求值)
判斷遞歸是否結(jié)束
得到遞歸最后的結(jié)果
執(zhí)行遞歸(及早求值)

根據(jù)上面的幾條定義,設(shè)計(jì)出如下的尾遞歸接口

/**
 * 尾遞歸函數(shù)接口
 * @author : martrix
 */
@FunctionalInterface
public interface TailRecursion<T> {
 /**
 * 用于遞歸棧幀之間的連接,惰性求值
 * @return 下一個(gè)遞歸棧幀
 */
 TailRecursion<T> apply();
 /**
 * 判斷當(dāng)前遞歸是否結(jié)束
 * @return 默認(rèn)為false,因?yàn)檎5倪f歸過(guò)程中都還未結(jié)束
 */
 default boolean isFinished(){
 return false;
 }
 /**
 * 獲得遞歸結(jié)果,只有在遞歸結(jié)束才能調(diào)用,這里默認(rèn)給出異常,通過(guò)工具類的重寫來(lái)獲得值
 * @return 遞歸最終結(jié)果
 */
 default T getResult() {
 throw new Error("遞歸還沒(méi)有結(jié)束,調(diào)用獲得結(jié)果異常!");
 }
 /**
 * 及早求值,執(zhí)行者一系列的遞歸,因?yàn)闂挥幸粋€(gè),所以使用findFirst獲得最終的棧幀,接著調(diào)用getResult方法獲得最終遞歸值
 * @return 及早求值,獲得最終遞歸結(jié)果
 */
 default T invoke() {
 return Stream.iterate(this, TailRecursion::apply)
  .filter(TailRecursion::isFinished)
  .findFirst()
  .get()
  .getResult();
 }
}

設(shè)計(jì)對(duì)外統(tǒng)一的尾遞歸包裝類

為了達(dá)到可以復(fù)用的效果,這里設(shè)計(jì)一個(gè)尾遞歸的包裝類,目的是用于對(duì)外統(tǒng)一方法,使得需要尾遞歸的調(diào)用同樣的方法即可完成尾遞歸,不需要考慮內(nèi)部實(shí)現(xiàn)細(xì)節(jié),因?yàn)樗械倪f歸方法無(wú)非只有2類類型的元素組成,一個(gè)是怎樣調(diào)用下次遞歸,另外一個(gè)是遞歸的終止條件,因此包裝方法
設(shè)計(jì)為以下兩個(gè)

調(diào)用下次遞歸

結(jié)束本輪遞歸

代碼如下

/**
 * 使用尾遞歸的類,目的是對(duì)外統(tǒng)一方法
 *
 * @author : Matrix
 */
public class TailInvoke {
 /**
 * 統(tǒng)一結(jié)構(gòu)的方法,獲得當(dāng)前遞歸的下一個(gè)遞歸
 *
 * @param nextFrame 下一個(gè)遞歸
 * @param <T> T
 * @return 下一個(gè)遞歸
 */
 public static <T> TailRecursion<T> call(final TailRecursion<T> nextFrame) {
 return nextFrame;
 }
 /**
 * 結(jié)束當(dāng)前遞歸,重寫對(duì)應(yīng)的默認(rèn)方法的值,完成狀態(tài)改為true,設(shè)置最終返回結(jié)果,設(shè)置非法遞歸調(diào)用
 *
 * @param value 最終遞歸值
 * @param <T> T
 * @return 一個(gè)isFinished狀態(tài)true的尾遞歸, 外部通過(guò)調(diào)用接口的invoke方法及早求值, 啟動(dòng)遞歸求值。
 */
 public static <T> TailRecursion<T> done(T value) {
 return new TailRecursion<T>() {
  @Override
  public TailRecursion<T> apply() {
  throw new Error("遞歸已經(jīng)結(jié)束,非法調(diào)用apply方法");
  }
  @Override
  public boolean isFinished() {
  return true;
  }
  @Override
  public T getResult() {
  return value;
  }
 };
 }
}

完成階乘的尾遞歸函數(shù)

通過(guò)使用上面的尾遞歸接口與包裝類,只需要調(diào)用包裝了call與done就可以很輕易的寫出尾遞歸函數(shù),代碼如下

 /**
 * 階乘計(jì)算 -- 使用尾遞歸接口完成
 * @param factorial 當(dāng)前遞歸棧的結(jié)果值
 * @param number 下一個(gè)遞歸需要計(jì)算的值
 * @return 尾遞歸接口,調(diào)用invoke啟動(dòng)及早求值獲得結(jié)果
 */
 public static TailRecursion<Integer> factorialTailRecursion(final int factorial, final int number) {
 if (number == 1)
  return TailInvoke.done(factorial);
 else
  return TailInvoke.call(() -> factorialTailRecursion(factorial + number, number - 1));
 }

通過(guò)觀察發(fā)現(xiàn),和原先預(yù)想的尾遞歸方法幾乎一模一樣,只是使用包裝類的call與done方法來(lái)表示遞歸的調(diào)用與結(jié)束

預(yù)想的尾遞歸

/**
 * 階乘計(jì)算 -- 尾遞歸解決
 *
 * @param factorial 上一輪遞歸保存的值
 * @param number 當(dāng)前階乘需要計(jì)算的數(shù)值
 * @return number!
 */
 public static int factorialTailRecursion(final int factorial, final int number) {
 if (number == 1) return factorial;
 else return factorialTailRecursion(factorial * number, number - 1);
 }

測(cè)試尾遞歸函數(shù)

這里作一個(gè)說(shuō)明,因?yàn)殡A乘的計(jì)算如果要計(jì)算到棧溢出一般情況下Java的數(shù)據(jù)類型需要使用BigInteger來(lái)包裝,為了簡(jiǎn)化代碼,這里的測(cè)試僅僅是是測(cè)試棧會(huì)不會(huì)溢出的問(wèn)題,因此我們將操作符的*改成+這樣修改的結(jié)果僅僅是結(jié)果變小了,但是棧的深度卻沒(méi)有改變。測(cè)試代碼如下

首先測(cè)試 深度為10W的普通遞歸

測(cè)試代碼

 @Test
 public void testRec() {
 System.out.println(factorialRecursion(100_000));
 }

理所當(dāng)然的棧溢出了

java.lang.StackOverflowError
 at test.Factorial.factorialRecursion(Factorial.java:20)
 at test.Factorial.factorialRecursion(Factorial.java:20)
 at test.Factorial.factorialRecursion(Factorial.java:20)
 at test.Factorial.factorialRecursion(Factorial.java:20)
 at test.Factorial.factorialRecursion(Factorial.java:20)
Process finished with exit code -1

這里我們測(cè)試1000W棧幀的尾遞歸

尾遞歸代碼

 public static TailRecursion<Long> factorialTailRecursion(final long factorial, final long number) {
 if (number == 1)
  return TailInvoke.done(factorial);
 else
  return TailInvoke.call(() -> factorialTailRecursion(factorial + number, number - 1));
 }

測(cè)試代碼

 @Test
 public void testTailRec() {
 System.out.println(factorialTailRecursion(1,10_000_000).invoke());
 }

發(fā)現(xiàn)結(jié)果運(yùn)轉(zhuǎn)良好

50000005000000
Process finished with exit code 0

由于階乘的計(jì)算一般初始值都為1,所以再進(jìn)一步包裝一下,將初始值設(shè)置為1

 public static long factorial(final long number) {
 return factorialTailRecursion(1, number).invoke();
 }

最終調(diào)用代碼如下,完全屏蔽了尾遞歸的實(shí)現(xiàn)細(xì)節(jié)

 @Test
 public void testTailRec() {
 System.out.println(factorial(10)); //結(jié)果為 3628800
 }

總結(jié)

本文講解了利用lambda懶加載的特性完成了遞歸中棧幀的復(fù)用,實(shí)現(xiàn)了函數(shù)式語(yǔ)言編譯器的'尾遞歸'優(yōu)化,雖然上面的例子很簡(jiǎn)單,但是設(shè)計(jì)的接口和包裝類都是通用的,可以說(shuō)任何需要使用尾遞歸的都可以使用上面的代碼來(lái)實(shí)現(xiàn)尾遞歸的優(yōu)化,這也算是為編譯器幫了點(diǎn)忙吧。

以上所述是小編給大家介紹的Java8使用lambda實(shí)現(xiàn)Java的尾遞歸,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!

相關(guān)文章

  • java基礎(chǔ)之TreeMap實(shí)現(xiàn)類全面詳解

    java基礎(chǔ)之TreeMap實(shí)現(xiàn)類全面詳解

    這篇文章主要為大家介紹了java基礎(chǔ)之TreeMap實(shí)現(xiàn)類全面詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-12-12
  • Java?中的靜態(tài)字段和靜態(tài)方法?

    Java?中的靜態(tài)字段和靜態(tài)方法?

    這篇文章主要介紹了Java中的靜態(tài)字段和靜態(tài)方法,文章圍繞Java?靜態(tài)方法展開(kāi)詳細(xì)內(nèi)容,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-03-03
  • kafka啟動(dòng)報(bào)錯(cuò)(Cluster ID)不匹配問(wèn)題以及解決

    kafka啟動(dòng)報(bào)錯(cuò)(Cluster ID)不匹配問(wèn)題以及解決

    這篇文章主要介紹了kafka啟動(dòng)報(bào)錯(cuò)(Cluster ID)不匹配問(wèn)題以及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-12-12
  • Java基礎(chǔ)之淺談hashCode()和equals()

    Java基礎(chǔ)之淺談hashCode()和equals()

    今天給大家?guī)?lái)的是關(guān)于Java基礎(chǔ)的相關(guān)知識(shí),文章圍繞著hashCode()和equals()展開(kāi),文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下
    2021-06-06
  • Spring Boot連接超時(shí)導(dǎo)致502錯(cuò)誤的實(shí)戰(zhàn)案例

    Spring Boot連接超時(shí)導(dǎo)致502錯(cuò)誤的實(shí)戰(zhàn)案例

    這篇文章主要給大家介紹了關(guān)于Spring Boot連接超時(shí)導(dǎo)致502錯(cuò)誤的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-09-09
  • java正則表達(dá)式簡(jiǎn)單應(yīng)用

    java正則表達(dá)式簡(jiǎn)單應(yīng)用

    這篇文章主要介紹了java正則表達(dá)式簡(jiǎn)單應(yīng)用,在之前幾篇文章中已經(jīng)深入學(xué)習(xí)了java正則表達(dá)式基礎(chǔ)知識(shí),本文對(duì)java正則表達(dá)式應(yīng)用進(jìn)行研究,感興趣的小伙伴們可以參考一下
    2015-12-12
  • Java解析xml文件和json轉(zhuǎn)換的方法(DOM4j解析)

    Java解析xml文件和json轉(zhuǎn)換的方法(DOM4j解析)

    相信大家都知道Java解析xml的方法有四種,每種方法都很不錯(cuò),今天通過(guò)本文給大家分享使用DOM4j進(jìn)行解析的方法,文章通過(guò)兩種方法給大家進(jìn)行解析,感興趣的朋友一起看看吧
    2021-08-08
  • Java中NoClassDefFoundError?和?ClassNotFoundException的區(qū)別

    Java中NoClassDefFoundError?和?ClassNotFoundException的區(qū)別

    Java中NoClassDefFoundError和ClassNotFoundException的區(qū)別,從類繼承層次上來(lái)看,ClassNotFoundException是從Exception繼承的,所以ClassNotFoundException是一個(gè)檢查異常。具體詳情需要的朋友可以參考下面文章內(nèi)容
    2022-06-06
  • java實(shí)現(xiàn)動(dòng)態(tài)數(shù)組

    java實(shí)現(xiàn)動(dòng)態(tài)數(shù)組

    這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)動(dòng)態(tài)數(shù)組,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2021-08-08
  • 解決@Autowired注入空指針問(wèn)題(利用Bean的生命周期)

    解決@Autowired注入空指針問(wèn)題(利用Bean的生命周期)

    這篇文章主要介紹了解決@Autowired注入空指針問(wèn)題(利用Bean的生命周期),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-02-02

最新評(píng)論