遞歸的技巧以及注意事項

什麼是遞歸

遞歸是一種非常高效、簡潔的編碼技巧,一種應用非常廣泛的算法。

比如DFS深度優先搜索、前中後序二叉樹遍歷等都是使用遞歸。

 

方法或函數調用自身的方式稱爲遞歸調用,調用稱爲遞,返回稱爲歸。

什麼問題可以用遞歸解決?

1.一個問題的解可以分解爲幾個子問題的解

2.這個問題與子問題除了數據規模不同之外,求解思路要相同

3.存在遞歸終止條件

 

寫遞歸代碼的技巧

1.寫出遞推公式

2.找到中止條件

 

eg. 經典的青蛙跳臺階問題:這裏有 n 個臺階,每次青蛙可以跨 1 個臺階或者 2 個臺階,請問走這 n 個臺階有多少種走法?

實際上,可以根據第一步的走法把所有走法分爲兩類。

第一類是第一步走了 1 個臺階,另一類是第一步走了 2 個臺階。

所以 n 個臺階的走法就等於先走 1 階後,n-1 個臺階的走法 加上先走 2 階後,n-2 個臺階的走法。

1.得出公式: f(n) = f(n-1)+f(n-2)

2.中止條件:當只剩一個臺階的時候只有一種走法:f(1) =1。剩兩個臺階的時候兩種走法:f(2) =2。

寫遞歸代碼的關鍵就是找到如何將大問題分解爲小問題的規律,並且基於此寫出遞推公式,然後再推敲終止條件

最後將遞推公式和終止條件翻譯成代碼。

int f(int n) {
  if (n == 1) return 1;
  if (n == 2) return 2;
  return f(n-1) + f(n-2);
}

 

理解遞歸的技巧

通常覺得遞歸很難理解,主要的原因是總想試圖想搞清楚,每一層是怎麼調用的,又是怎麼返回的。一層一層的去想調用和返回,很容易被繞進去。

編寫遞歸代碼的關鍵是,只要遇到遞歸,就把它抽象成一個遞推公式,不用想一層層的調用關係,不要試圖用人腦去分解遞歸的每個步驟。

如果一個問題 A 可以分解爲若干子問題 B、C、D,你可以假設子問題 B、C、D 已經解決,在此基礎上思考如何解決問題 A。

而且,你只需要思考問題 A 與子問題 B、C、D 兩層之間的關係即可,不需要一層一層往下思考子問題與子子問題,子子問題與子子子問題之間的關係。屏蔽掉遞歸細節,這樣子理解起來就簡單多了。

 

遞歸的注意事項

 

警惕堆棧溢出

如果遞歸求解的數據規模很大,調用層次很深,一直壓入棧,就會有堆棧溢出的風險。

Exception in thread "main" java.lang.StackOverflowError

可以通過在代碼中限制遞歸調用的最大深度的方式來解決這個問題。

例如遞歸調用超過一定深度(比如 1000)之後,我們就不繼續往下再遞歸了,直接返回報錯。

因爲最大允許的遞歸深度跟當前線程剩餘的棧空間大小有關,事先無法計算。

如果實時計算,代碼過於複雜,就會影響代碼的可讀性。

所以,如果最大深度比較小,比如 10、50,就可以用這種方法,否則這種方法並不是很實用。

 

警惕重複計算

分解一下之前青蛙跳臺階的例子

例如圖中的f(3),f(3)就被計算了很多次

 重複計算問題可以利用某種數據結構來保存已經求解過的 f(k),例如HashMap。

當遞歸調用到 f(k) 時,先看下是否已經求解過了。如果是,則直接從散列表中取值返回,這樣就能避免重複計算。

 

遞歸的優缺點

 

1.優點:代碼的表達力很強,寫起來簡潔。
2.缺點:空間複雜度高、有堆棧溢出風險、存在重複計算、過多的函數調用會耗時較多等問題。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章