【演算法】遞迴演算法

玉古路38號發表於2020-11-09

 

1. 什麼是遞迴?

下面先描述一個遞迴可能應用的實際場景:

我們有一個神祕的盒子,有一把鑰匙在這個盒子裡。

這個盒子 裡有盒子,而盒子裡面又有盒子。鑰匙可能就在某一個盒子中。為了找到鑰匙,有兩種解決思路。

下面是第一種解決方式:

  1. 建立一個要查詢的盒子堆;
  2. 從盒子堆取出一個盒子,在裡面找;
  3. 如果找到的是盒子,則將盒子加入盒子堆中,以便以後再查詢;
  4. 如果找到鑰匙,則大功告成!
  5. 回到第二步。

另一種解決方法則更為清晰:

  1. 檢查盒子中的每樣東西;
  2. 如果是盒子,就回到第一步;
  3. 如果是鑰匙,就大功告成!

對於實際上操作,我想大多數人都會採用第二種方法,而不是採用拿起又放下的第一種操作。所以說,雖然我們不知道我們在用遞迴,而實際上我們經常在用遞迴解決生活上的問題。

對於第一種方法使用while迴圈:只要盒子堆不空,就從中取盒子,並在其中仔細查詢。

def look_for_key(main_box):
  pile = main_box.make_a_pile_to_look_through()
  while pile is not empty:
    box = pile.grab_a_box()
    for item in box:
      if item.is_a_box():
        pile.append(item)
      elif item.is_a_key():
        print "found the key!"

 第二種方法使用遞迴——函式呼叫自己,這種方法的虛擬碼如下。

def look_for_key(box):
  for item in box:
    if item.is_a_box():
      look_for_key(item)
    elif item.is_a_key():
      print "found the key!

 這兩種方法的作用相同,但在我看來,第二種方法更清晰。遞迴只是讓解決方案更清晰,並沒有效能上的優勢。實際上,在有些情況下,使用迴圈的效能更好。正如大牛所言:“如果使用迴圈,程式的效能可能更高;如果使用遞迴,程式可能更容易理解。如何選擇要看什麼對你來說更重要。”

2. 基線條件和遞迴條件

因為遞迴函式要呼叫自己,旖旎次編寫這樣的函式時很容易出錯,進而導致無限迴圈。例如,要編寫一個如下的倒數計時的函式:

> 4、 3、 2、 1

為此,你可以用遞迴的方式編寫,如下所示:

def countdown(i):
  print i
  countdown(i-1)

如果執行上述程式碼,將發現一個問題:這個函式執行起來沒完沒了!

 > 3...2...1...0...-1...-2...

編寫遞迴函式時,必須告訴它如何停止遞迴。正因為如此,每個遞迴函式都用兩個部分:基線條件(base case)和遞迴條件(recursive case)。遞迴條件指的是函式呼叫自己,而基線條件則指的是函式不再呼叫自己,從而避免形成無限迴圈。

給上面的倒數計時函式新增基線條件:

def countdown(i):
  print i
  if i <= 0:
    return
  else:
    countdown(i-1)

現在,函式將像預期那樣執行:

 3. 遞迴呼叫棧

我們知道,在一個函式執行過程中呼叫另一個函式時,在計算機內部會使用使用一塊結構為棧(把棧想象成疊盤子的倉,先放進去,先拿出來)的呼叫棧。遞迴函式也使用呼叫棧!來看看計算階乘的遞迴函式factorial的呼叫棧。factorial(5)表示5! = 5*4*3*2*1。下面即為計算階乘的遞迴函式:

def fact(x):
  if x == 1:
    return 1
  else:
    return x * fact(x-1)

下面來詳細探討呼叫factorial(3)的呼叫棧是如何變化的。 棧頂的方框指出了當前執行到什麼地方。

 注意,每個fact呼叫都有自己的x變數。在一個函式呼叫中不能訪問另一個的x變數。棧在遞迴中扮演著重要角色。開頭曾提及兩種尋找鑰匙的方法,下面列出第一種方法。

使用這種方法時,你建立一個待查詢的?子堆,因此你始終知道還有哪些?子需要查詢。

使用遞迴時,則沒有盒子堆。

因為存在一個盒子堆,因此你始終知道哪些盒子需要查詢。

如果沒有盒子堆,那如何知道還有哪些盒子需要查詢呢?

此時,呼叫棧類似於下面這樣:

此時,相當於“盒子棧”儲存在棧中了!這個棧包含未完成的函式呼叫,每個函式呼叫都包含還未檢查完的盒子。使用棧很方便,它自動幫你跟蹤了盒子堆。

 使用棧雖然很方便,但是要付出代價:儲存詳盡的資訊可能要佔用大量的記憶體。每個函式呼叫都要佔用一定的記憶體,如果棧很高,就意味著計算機儲存了大量函式呼叫的資訊。在這種情況下,有兩個替代方案:

  1. 重新編寫程式碼,轉而使用迴圈;
  2. 使用 尾遞迴 。這是一個高階遞迴,並非所有語言都支援。

 

 

 

參考《圖解演算法》,為學習筆記。

相關文章