一道面試題引發的思考(遞迴)
前言
某日,去某網際網路公司面試,被問到了如下一道面試題:
題目很簡單,有一隊人,已知第一個人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 異常。