? ? 1遞歸需要滿足的三個條件
? ? ? ? 1.一個問題的解可以分解為幾個子問題的解。
? ? ? ? 2.這個問題與分解之后的子問題,除了數據規模不同,求解思路完全一樣。
? ? ? ? 3.存在遞歸終止條件。
????2如何編寫遞歸代碼?
????????寫遞歸代碼最關鍵的是寫出遞歸公式,找到終止條件,然后再將遞推公式轉化為代碼。
? ? ? ? 用一個例子來理解~
? ? ? ? 假設有n個臺階,每次可以跨1個臺階或者2個臺階,那么這n個臺階有多少種走法?
? ? ? ? 實際上,可以根據第一步的走法把所有走法分為兩類,第一類是第一步走了1個臺階, 第二類是第一步走了2個臺階。所以,n個臺階的走法=先走1階后剩下n-1個臺階的走法+先走2階后剩下n-2個臺階的走法,公式如下:
? ? ? ? f(n)=f(n-1)+f(n-2)? ?
? ? ? ? 終止條件為:f(1)=1,f(2)=2.
? ? ? ? 轉化成代碼:
? ? ? ? 寫遞歸代碼的關鍵就是找到如何將大問題分解為小問題的規律,并且基于此寫出遞推公式,然后再推敲終止條件,最后將遞推公式和終止條件翻譯成代碼。
? ? ? ? 遞歸分為兩個過程,去的過程叫“遞”,回來的過程叫“歸”。
? ? ? ? 計算機擅長做重復的事情,所以遞歸正和它的胃口。
? ? 3遞歸代碼要警惕堆棧溢出
? ? ? ? 之前學習“棧”的時候,我們知道函數調用會使用棧來保存臨時變量,每調用一個函數,都會將臨時變量封裝為棧幀壓入內存棧,等函數執行完成返回時,才出棧。系統棧或者虛擬機棧空間一般都不大,而如果遞歸求解的數據規模很大、調用層次很深的話,一直壓入棧就會有堆棧溢出的風險。
? ? ? ? 如何避免出現堆棧溢出呢?
? ? ? ? 可以通過在代碼中限制遞歸調用的最大深度的方式來解決這個問題。遞歸調用超過一定深度(比如1000)之后,我們就不繼續往下再遞歸了,直接返回報錯。
? ? ? ? 一個偽代碼的例子:
? ? ? ? 但這種做法并不能完全解決問題,因為最大允許的遞歸深度跟當前線程剩余的棧空間大小有關,事先無法計算。如果實時計算,代碼過于復雜,會影響代碼的可讀性。所以如果最大深度比較小,就可以用這種方法,否則這種方法并不是很實用。
? ? 4遞歸代碼要警惕重復計算
? ? ? ? 使用遞歸的時候還會出現重復計算的問題,比如我們剛才講的例子:
? ? ? ? 可以看到,同一個f(k)我們會計算好幾次。為了避免重復計算,我們可以通過一個數據結構(比如散列表)來保存已經求解過的f(k)。每次遞歸調用到f(k)的時候,先看下是否已經求解過了,如果是,則直接從散列表中取值返回。
? ? ? ? OK,優化后的代碼如下:
? ? 5怎么將遞歸代碼改寫為非遞歸代碼?
? ? ? ? 遞歸有利有弊,利是遞歸代碼的表達能力強,寫起來非常簡潔;弊是空間復雜度高、有堆棧溢出的風險、存在重復計算、過多的函數調用會耗時較多等問題。所以在開發過程中,我們要根據實際情況來選擇是否需要通過遞歸的方式來實現。
? ? ? ? 那如何將遞歸代碼改寫為非遞歸代碼呢?
? ? ? ? 例一:
? ? ? ? 遞歸代碼:
? ? ? ? 非遞歸代碼:
? ? ? ? 例二(上臺階的例子):
? ? ? ? 遞歸代碼:
? ? ? ? 非遞歸代碼:
? ? ? ? 所有的遞歸代碼都可以改為這種迭代循環的非遞歸寫法嗎?
? ? ? ? 是的。
? ? ? ? 因為遞歸本身就是借助棧來實現的,只不過我們使用的棧是系統或者虛擬機本身提供的,我們沒有感知罷了。如果我們自己在內存堆上實現棧,手動模擬入棧、出棧過程,這樣任何遞歸代碼都可以改寫成看上去不是遞歸代碼的樣子。
? ? ? ? 但是這種思路實際上是將遞歸改為了“手動”遞歸,本質并沒有變,而且也并沒有解決前面講到的某些問題,徒增了實現的復雜度。
????內容小結
? ? ? ? 遞歸是一種非常高效、簡潔的編碼技巧,只要滿足“三個條件”的問題就可以通過遞歸代碼來解決。不過遞歸代碼也比較難寫、難理解。編寫遞歸代碼的正確姿勢是寫出遞推公式,找出終止條件,然后翻譯成遞歸代碼。遞歸代碼雖然簡潔高效,但是也有很多弊端,比如堆棧溢出、重復計算、函數調用耗時多、空間復雜度高等,所以在編寫遞歸代碼的時候,一定要控制好這些副作用。