1. 程式人生 > >10 遞迴 Recursion:如何用三行程式碼找到“最終推薦人”

10 遞迴 Recursion:如何用三行程式碼找到“最終推薦人”

之後的DFS深度優先搜尋、前中後序二叉樹遍歷等都要用到遞迴

一、如何理解遞迴? 遞推公式:

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

遞迴程式碼:

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

二、遞迴需要滿足三個條件 1、一個問題的解可以分解為幾個子問題的解 2、這個問題與分解之後的子問題,除了資料規模不同,求解思路完全一樣 3、存在遞迴終止條件

三、如何編寫遞迴程式碼?

四、遞迴程式碼要警惕堆疊溢位 函式呼叫會使用棧來儲存臨時變數。每呼叫一個函式,都會將臨時變數封裝為棧幀壓入記憶體棧,等函式執行完成返回時,才出棧。系統棧或者虛擬機器棧空間一般都不大。如果遞迴求解的資料規模很大,呼叫層次很深,一直壓入棧,就會有堆疊溢位的風險。

Exception in thread "main" java.lang.StackOverflowError

解決方式:在程式碼中限制遞迴呼叫的最大深度。當遞迴呼叫超過一定深度之後,就不繼續往下再遞迴,直接返回報錯。

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

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

但這種做法並不能完全解決問題,因為最大允許的遞迴深度跟當前執行緒剩餘的棧空間大小有關,事先無法計算,程式碼過於複雜,就會影響程式碼的可讀性。所以,如果最大深度比較小,比如10、50,就可以用這種方法,否則並不實用。

五、遞迴程式碼要警惕重複計算 可以通過一個數據結構(比如散列表)來儲存已經求解過的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 hasSovledList.get(n);
  }
 
  int ret = f(n-1) + f(n-2);
  hasSovledList.put(n, ret);
  return ret;
}

在時間效率上,遞迴程式碼多了很多函式呼叫,當中函式呼叫的數量較大時,就會集聚成一個可觀的時間成本。在空間複雜度上,因為遞迴呼叫一次就會在記憶體棧中儲存一次現場資料,所以在分析遞迴程式碼空間複雜度時,需要額外考慮這部分的開銷。

六、怎麼將遞迴程式碼改寫為非遞迴程式碼? 遞迴有利有弊,利是遞迴程式碼表達力很強,寫起來非常簡潔;弊就是空間複雜度高、有堆疊溢位的風險、存在重複計算、過多的函式呼叫會耗時較多等問題。 f(x) =f(x-1)+1 這個遞推公式 改寫

int f(int n) {
  int ret = 1;
  for (int i = 2; i <= n; ++i) {
    ret = ret + 1;
  }
  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;
}

實際本質還是遞迴

問題:如何找到“最終推薦人”?

long findRootReferrerId(long actorId) {
  Long referrerId = select referrer_id from [table] where actor_id = actorId;
  if (referrerId == null) return actorId;
  return findRootReferrerId(referrerId);
}

除錯遞迴程式碼: 1.列印日誌發現,遞迴值。 2.結合條件斷點進行除錯。