數據結構與算法之美-遞歸

前言:本篇文章只是記錄王爭的數據結構與算法之美的學習筆記,寫下來能強迫自己系統的再過一遍,加深理解。這門課以實際開發中遇到的問題為例,引入解決問題涉及到的的數據結構和算法,但不會講的太細,最好結合一本實體書進行學習。

在求解鏈表的問題時,經常會用到遞歸,并且之后的 DFS 深度優先搜索、前中后序二叉樹遍歷等,所以我們必須要了解遞歸。

1. 遞歸

去的過程叫"遞",回來的過程叫"歸",基本上,所有的遞歸問題都可以用遞推公式來表示。

舉個例子,看電影去時,不知道自己是第幾排了,可以問前面一排的人他是第幾排,只要在他的數字上+1,就可以了,但是前面的人也不清楚,所以他也問他前面的人,這樣一排一排的問,直到問道第一排,第一排的人說我在第一排,然后一排一排的再把數字傳回來,直到你前面的人告訴你他在哪一排,于是就知道答案了,我們用遞推公式表示出來:

f(n) = f(n-1)+1,其中 f(1) = 1

改成遞歸代碼如下:

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

2. 遞歸需要滿足的三個條件

  • 一個問題的解可以分解為幾個子問題的解
  • 這個問題與分解之后的子問題,除了數據規模不同,求解思路完全一樣
  • 存在遞歸終止條件

寫遞歸代碼最關鍵的是寫出遞推公式,找到終止條件,剩下將遞推公式轉化為代碼就很簡單了。

比如上臺階的問題,每次可以跨 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,所以遞歸代碼如下所示:

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

寫遞歸代碼的關鍵就是:

  • 1.找到如何將大問題分解為小問題的規律
  • 2.并且基于此寫出遞推公式
  • 3.然后再推敲終止條件
  • 4.最后將遞推公式和終止條件翻譯成代碼

另外,不要試圖想清楚整個遞和歸的過程,人腦沒辦法都想清楚,如果一個問題A 可以分解為若干子問題 B C D,可以假設 B C D 已經解決,然后在此基礎上思考如何解決問題 A,不需要一層一層往下思考子問題和子子問題的關系,要屏蔽掉細節

只要遇到遞歸,就把它抽象成一個遞推公式,不用想一層層的調用關系。

3. 注意事項

3.1 遞歸代碼警惕堆棧溢出

遞歸就是自己調用自己,函數調用會使用棧來保存臨時變量,每調用一次函數,都會將臨時變量封裝成棧幀壓入內存棧,等函數執行完成返回時,才出棧。如果遞歸求解的數據規模很大,調用層次很深,就會有堆棧溢出的風險。

那么如何避免呢?
可以限制遞歸調用的最大深,遞歸調用超過一定深度之后,就不繼續遞歸了,直接返回報錯,比如電影院的那個例子:


// 全局變量,表示遞歸的深度。
int depth = 0;

int f(int n) {
  ++depth;
  if (depth > 1000) throw exception;
  
  if (n == 1) return 1;
  return f(n-1) + 1;
}

如果最大深度比較小,可以用此種方法。

3.2 警惕重復計算

第二個上臺階問題的遞歸過程分分解圖:


image.png

從圖中可以看到,想要計算f(5),需要先計算 f(4) 和 f(3),而計算 f(4) 還需要計算 f(3),因此,f(3) 就被計算了很多次,這就是重復計算問題。

我們可以用散列表來解決這個問題,保存已經求解過的 f(k),當遞歸調用到 f(k)時,如果之前已經求解過,則直接從散列表中取值,不需要重復計算,代碼如下:


public int f(int n) {
  if (n == 1) return 1;
  if (n == 2) return 2;
  
  // hasSolvedList可以理解成一個Map,key是n,value是f(n)
  if (hasSolvedList.containsKey(n)) {
    return hasSolvedList.get(n);
  }
  
  int ret = f(n-1) + f(n-2);
  hasSolvedList.put(n, ret);
  return ret;
}

在空間復雜度的計算上,遞歸調用一次就會在內存棧中保存一次現場數據,所以上面講到的電影院的遞歸代碼,空間復雜度為 O(n)。

遞歸優點:

  • 表達力強
  • 代碼簡潔

遞歸缺點:

  • 空間復雜度高
  • 有堆棧溢出風險
  • 存在重復計算、過多函數調用導致耗時較多等問題

4. 遞歸改為非遞歸

所有的遞歸代碼都可以改為迭代循環的非遞歸寫法遞歸本身就是借助棧來實現的,只不過使用的棧是系統或者虛擬機提供的。如果我們自己實現棧,手動模擬入棧、出棧過程,這樣就可以了。

5. 總結

遞歸就是借助棧實現的,需要注意它的空間復雜度。
寫遞歸代碼先要寫出遞推公式,然后找出終止條件,再翻譯成遞歸代碼。
要注意堆棧溢出、重復計算、函數調用耗時多、空間復雜度高問題。

遞歸是需要配合其他數據結構來進行的,在以后的練習題中會經常遇到遞歸了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容