1. 程式人生 > >一道面試題引發的思考(遞迴)

一道面試題引發的思考(遞迴)

前言

某日,去某網際網路公司面試,被問到了如下一道面試題:

題目很簡單,有一隊人,已知第一個人8歲,後一個人比前一個人大兩歲,以此類推,問第8個人多少歲?第N個人多少歲。

我拿過筆和紙,不假思索的寫下了如下答案:

    static int getAge(int n){
        if(n==1){
            return 8;
        }else{
            return getAge(n-1)+2;
        }
    }

面試官又問我還有什麼需要注意的嗎?我說要注意引數不能小於1.

顯然,面試官不是很滿意,又問我如果N很大會怎麼樣?

我想了想,說,會出現OOM異常吧,或者超了int的範圍。

感覺他還是不滿意。

思考

自那過去一段時間,我自認為答得沒有問題,也答到了點上(只可惜面試沒過O(∩_∩)O哈哈)。

最近又想到了這個問題,決定研究研究。

實踐

我用自己的方法,進行了資料測試。當然我把為了測試效果明顯,我加大了測試值。

public static void main(String[] args) { 
	System.out.println(getAge(1000000));
}

結果使我驚訝,不是OOM異常,而是堆疊異常。

Exception in thread "main" java.lang.StackOverflowError

後面我瞭解到,遞迴呼叫,可以假想成一個函式呼叫另一個函式,而每個函式相當於佔用一個棧幀,這些棧幀以先進後出的方式排列起來形成棧。如下圖:

在這裡插入圖片描述

這樣,函式會追尋到棧頂,拿到getAge(1)的值後逐漸返回。如下。

getAge(1)=8
getAge(2)=getAge(1)+2=10
getAge(3)=getAge(2)+2=12
getAge(4)=getAge(3)+2=14

可以知道,如果堆疊深度不夠的話,就會出現異常。

我們上圖所示的異常就是這個原因。

提升

改進一

在研究這個問題時,我發現了一種遞迴,尾遞迴。

如下所示:

    static int getAge1(int n,int result){
        if(n==1){
            return result;
        }else
{ return getAge1(n-1,result+2); } }

這種遞迴我們可以看到,帶了一個引數result,當他執行到n==1時,直接返回了result,不用在一層層回退進行計算。如下:

  getAge(4,8)
= getAge(3,8+2)
= getAge(2,8+2+2)
= getAge(1,8+2+2+2)
= 14

一些編譯器發現這些函式可以在一個棧幀裡進行完成,就會複用棧幀,優化程式碼。

可惜的是,到目前為止,JAVA在HotSpot(Oracle的JVM)上使用時,並不支援尾遞迴優化。 據說IBM的JVM支援尾遞迴優化,有興趣的童鞋可以試下。

所以這種方案的測試結果肯定也是StackOverflowError啦。

改進二

迴圈解決:

那時腦子笨,現在想想,這道題用迴圈也是可以解決的,且不用擔心堆疊溢位問題。如下:

While迴圈:

    static int getAge2(int n,int result,int step){
        while(n>1){
            result+=step;
            n--;
        }
        return result;
    }

For迴圈:

    static int getAge3(int start,int end,int firstValue,int step){
        for(int i=start;i<end;i++){
            firstValue+=step;
        }
        return firstValue;
    }

且速度也提高了不少。

改進三

這麼有規律的資料,當然乘法就可以解決,如下:

    static int getAge4(int start,int end,int firstValue,int step){
        return firstValue+(end-start)*step;
    }

以上例子沒有對入參做校驗處理,結合實際場景,其實需要處理的,在此略掉了。

總結

JDK原始碼中很少有遞迴,因為遞迴不能被優化,當資料過大時,很容易出現堆疊溢位,我們也應該慎用,基本能有遞迴解決的問題也可以用迴圈解決。

可以看出,對於一個問題,實現的方案可能不止一種。

看到自己想了及總結了多種方法,感覺很詫異,面試時卻只想到了遞迴一種,而且實現很不好。還是自己基礎不夠紮實,應該多學多看多練。

其他

上面都說要多學多看多練啦…

這是我測試IBM JDK對於尾遞迴(getAge1方法)的結果。

在這裡插入圖片描述

在這裡插入圖片描述

說明IBM JDK確實對尾遞迴有優化,我又測試了getAge方法(普通遞迴),結果和Oracle JDK一樣,丟擲 java.lang.StackOverflowError 異常。