1. 程式人生 > >資料結構與演算法學習筆記之高效、簡潔的編碼技巧“遞迴”

資料結構與演算法學習筆記之高效、簡潔的編碼技巧“遞迴”

前言

盜夢空間想象大多數人都看過:電影講述的是主人公諾蘭進入希裡安·墨菲夢境植入想法的行動。為了向希裡安·墨菲夢植入理念,影片進入四層夢境,即所謂:“夢中的夢中 夢中人的夢中”。

有一對兔子,每隔三個月會產下一對小兔子,小免子每隔三個月,也會產生新的一對免子,問36個月後,共有多少對兔子。

諸如此類:其實就是“遞迴”,今天就一起進入“遞迴”的世界看看

正文

一、遞迴的定義


1.遞迴是一種應用廣泛的演算法,既能運用到軟體開發中成為高效、簡潔的編碼技巧也能應用到生活中解決實踐遞迴問題,比如DFS深度優先搜尋、前中後序二叉樹遍歷等,又比如計算不斷繁衍的後臺個數等等;

2.程式呼叫自身的方式稱為遞迴呼叫,去呼叫的過程稱為遞,回來的過程稱為歸。

3.基本上,所有的遞迴問題都可以用遞推公式來表示,比如

f(n) = f(n-1) + 1; 
f(n) = f(n-1) + f(n-2);
f(n)=n*f(n-1);

二、為什麼使用遞迴?

1.遞迴在解決某些問題的時候使得我們思考的方式得以簡化,程式碼也更加精煉,容易閱讀
2.遞迴在處理問題時要反覆呼叫函式,這增大了它的空間和時間開銷,空間複雜度高、有堆疊溢位風險、存在重複計算、過多的函式呼叫會耗時較多等問題。

三、什麼樣的問題可以用遞迴解決呢?

一個問題只要同時滿足以下3個條件,就可以用遞迴來解決:
1.問題的解可以分解為幾個子問題的解。何為子問題?就是資料規模更小的問題。


2.問題與子問題,除了資料規模不同,求解思路完全一樣
3.存在遞迴終止條件,不能無限迴圈。

四、如何實現遞迴

1.遞迴程式碼編寫


寫遞迴程式碼的關鍵就是將大問題分解為小問題,寫出遞推公式,找出終止條件,最後將遞推公式和終止條件翻譯成程式碼。

舉一個栗子:

假如這裡有n個臺階,每一次你可以跨過一或二個臺階,請問有幾種走法?

根據第一步的走法把走法分為兩類,第一步走一個臺階或者走兩個臺階,所以n個臺階的走法就等於先走一階的走法加上先走兩個臺階的走法,遞迴公式為:

f(n) = f(n-1)+f(n-2)

當只有一個臺階時,我們就不需要遞迴了,所以終止條件為:

f(1)=1

但是隻有它還不足夠,n=2時,f(2)=f(1)+f(0)還有f(0)=1,也就是第0階也要有一種走法,不和邏輯,所以終止條件還有一個:

f(2)=2

編寫為程式碼為:

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

2.遞迴程式碼理解

對於遞迴程式碼,若試圖想清楚整個遞和歸的過程,實際上是進入了一個思維誤區。
那該如何理解遞迴程式碼呢?如果一個問題A可以分解為若干個子問題B、C、D,你可以假設子問題B、C、D已經解決。而且,你只需要思考問題A與子問題B、C、D兩層之間的關係即可,不需要一層層往下思考子問題與子子問題,子子問題與子子子問題之間的關係。遮蔽掉遞迴細節,這樣子理解起來就簡單多了。
因此,理解遞迴程式碼,就把它抽象成一個遞推公式,不用想一層層的呼叫關係,不要試圖用人腦去分解遞迴的每個步驟。

五、遞迴常見問題及解決方案

1.警惕堆疊溢位:可以宣告一個全域性變數來控制遞迴的深度,從而避免堆疊溢位。

程式碼實現:

// 全域性變數,表示遞迴的深度。
int depth = 0;

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


2.警惕重複計算:通過某種資料結構來儲存已經求解過的值,從而避免重複計算。

如用散列表來儲存已存在的值,改寫上面的程式碼如下:

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 hasSovledList.get(n);
  }
  
  int ret = f(n-1) + f(n-2);
  hasSovledList.put(n, ret);
  return ret;
}

(程式碼來源於網路)

六、如何將遞迴改寫為非遞迴程式碼?

籠統的講,所有的遞迴程式碼都可以改寫為迭代迴圈的非遞迴寫法。如何做?抽象出遞推公式、初始值和邊界條件,然後用迭代迴圈實現。

將上面的程式碼實現如下:

int f(int n) {
  if (n == 1) return 1;
  if (n == 2) return 2;
  
  int ret = 0;
  int pre = 2;
  int prepre = 1;
  for (int i = 3; i <= n; ++i) {
    ret = pre + prepre;
    prepre = pre;
    pre = ret;
  }
  return ret;
}

相關文章

以上內容為個人的學習筆記,僅作為學習交流之用。

 

歡迎大家關注公眾號,不定時乾貨,只做有價值的輸出

版權:本文版權歸作者
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任