你們都知道遞歸,尾遞歸呢?什麼又是尾遞歸優化?

你們都知道遞歸,尾遞歸呢?什麼又是尾遞歸優化?

碼農唐磊 程序猿石頭
你們都知道遞歸,尾遞歸呢?什麼又是尾遞歸優化?
今天,咱們來聊聊遞歸函數。爲啥忽然想到遞歸?其實就從電影名字《恐怖遊輪》《盜夢空間》想到了。圖片java

遞歸是啥?

遞歸函數你們確定寫過,學校上課的時候,估計最開始的例子就是斐波拉契數列了吧。例如:面試

int Fibonacci(n) {
    if (n < 2) return n;
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

遞歸函數簡而言之就是在一個函數中,又「遞歸」調用本身。在寫遞歸函數的時候,須要注意的地方就是遞歸函數的結束條件。用遞歸函數確實能簡化不少算法的實現,好比常見的二叉樹遍歷等。但每每在寫遞歸函數的時候,最容易出現的問題就是所謂的「棧溢出」。算法

爲何會有「棧溢出」呢?由於函數調用的過程,都要藉助「棧」這種存儲結構來保存運行時的一些狀態,好比函數調用過程當中的變量拷貝,函數調用的地址等等。而「棧」每每存儲空間是有限的,當超過其存儲空間後,就會拋出著名的異常/錯誤「StackOverflowError」。ide

咱們以一個簡單的加法爲例,例如:函數

int sum(int n) {
    if (n <= 1) return n;
    return n + sum(n-1);
}

std::cout << sum(100) << std::endl;
std::cout << sum(1000000) << std::endl;

很簡答,編譯運行後,比較小的數字,能獲得正確的答案,當數字擴大後,就會直接發生「segmentation fault」。性能

尾遞歸又是啥?

我得知這個概念,最開始仍是由於不少年前一次面試,面試官問我「你知道什麼是尾遞歸嗎?」,我覺得是「僞」遞歸,難道是假的遞歸???當初我也是懵逼狀態(當初面試官忍住沒笑也是厲害了)。從「尾」字可看出來即若函數在尾巴的地方遞歸調用本身。上面的例子寫成尾遞歸,就變成了以下:優化

int tailsum(int n, int sum) {
    if (n == 0) return sum;
    return tailsum(n-1, sum+n);
}

能夠試試結果,計算從 1 加到 1000000,仍然是segmentation fault。爲何呢?由於這種寫法,本質上仍是有多層的函數嵌套調用,中間仍然有壓棧、出棧等佔用了存儲空間(只不過能比前面的方法會省部分空間)。scala

尾遞歸優化

當你給編譯選項開了優化以後,見證奇蹟的時刻到了,竟然能算出正確結果。如圖所示:
你們都知道遞歸,尾遞歸呢?什麼又是尾遞歸優化?code

C++ 默認 segmentation fault, 開啓編譯優化後,能正常計算結果。blog

緣由就是由於編譯器幫助作了尾遞歸優化,能夠打開彙編代碼看看(這裏就不展現 C++的了)。後面我用你們比較熟悉的 JVM based 語言 Scala 來闡述這個優化過程。(好像 Java 的編譯器沒作這方面的優化,至少我實驗我本地 JDK8 是沒有的,不清楚最新版本的有木有)(scala 自己提供了一個註解幫助編譯器強制校驗是否可以進行尾遞歸優化@tailrec)

object TailRecObject {

   def tailSum(n: Int, sum: Int): Int = {
        if (n == 0) return sum;
        return tailSum(n-1, n+sum);
   }

   def main(args: Array[String]) {
      println(tailSum(100, 0))
      println(tailSum(1000000, 0))
   }

}

結果以下圖所示,默認狀況下 scalac 作了尾遞歸優化,可以正確計算出結果,當經過 -g:notailcalls 編譯參數去掉尾遞歸優化後,就發生了 Exception in thread "main" java.lang.StackOverflowError了。
你們都知道遞歸,尾遞歸呢?什麼又是尾遞歸優化?

默認啓用尾遞歸優化正常計算結果,禁用尾遞歸優化則「StackOverflow」。
咱們來看看生成的字節碼有什麼不一樣。
你們都知道遞歸,尾遞歸呢?什麼又是尾遞歸優化?

包含尾遞歸優化的字節碼,直接 goto 循環。
你們都知道遞歸,尾遞歸呢?什麼又是尾遞歸優化?

禁用尾遞歸優化的字節碼,方法調用。

從上面能夠看出,尾遞歸優化後,變成循環了(前面的 C++ 相似)。

好了,尾遞歸我們就瞭解到這裏。我的見解,咱們知道有「尾遞歸」這個點就行了,有時候咱們寫遞歸就是爲了方便,代碼可讀性好,若是確實是出於性能考慮,咱們能夠本身用迭代的方式去實現,不依賴於具體的編譯器實現。固然對於像 scala 這樣,有一些語法糖可以幫助校驗和驗證,也是一個不錯的選擇。但遞歸轉迭代的能力,咱們能具有豈不更好。

下次想聊什麼話題嗎?歡迎留言。老規矩,若是有幫助(對你身邊的其餘人有幫助也行呀,一點幫助也沒有的話應該也不會看到這裏了吧),寫篇文章真心不易,但願親多多幫忙「在看」,轉發分享支持。

相關文章
相關標籤/搜索