【算法】算法圖解筆記_遞歸

遞歸是個有意思的概念,正如在前面所說,遞歸能讓算法的可讀性大大提升,並且一般要比使用循環結構更能寫出準確的算法。這本書形象引入了遞歸,並無太深刻,因此我進行了一點「添油加醋」。python

遞歸

概念

遞歸其實就是本身調用本身。能夠從多種維度對遞歸分類,我見過的最多見的分類:算法

  • 直接遞歸

本身直接調用本身。如:數據結構

--haskell
length' :: [a] -> Int
length' [] = 0
length' (_:xs) = 1 + length' xs

上面定義的length'就是經過直接遞調用自身完成列表長度的計算。app

  • 間接遞歸

能夠認爲只要不是直接調用本身的遞歸都是間接遞歸,其表現形式較多,如A->(調用)B,B->(調用)A,如奇偶謂詞函數:函數

--haskell
odd' :: Int -> Bool
odd' 0 = False
odd' n = even' (n - 1)

even' :: Int -> Bool
even' 0 = True
even' n = odd' (n - 1)

也能夠有A->B,B->C,... Z->A。這裏就不舉例子了。性能

書中的例子

有一個盒子,盒子裏套着一個或多個盒子,盒子裏的盒子又有盒子,依次類推。而鑰匙就在某個盒子裏,咱們怎麼找到鑰匙呢。

策略一

圖片描述

僞代碼以下:[僞代碼是對手頭問題的簡要描述,看着像代碼,但其實更接近天然語言。]學習

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!"

若是學習過樹的遍歷,是否是發現這就是廣度優先遍歷啊優化

策略二

圖片描述
僞代碼以下:spa

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!"

對應的就是深度優先遍歷啊code

這兩種方法的做用相同,但第二種方法更清晰。遞歸只是讓解決方案更清晰,並無性能上的優點。實際上,在有些狀況下,使用循環的性能更好。Leigh Caldwell在Stack Overflow上說的一句話:「若是使用循環,程序的性能可能更高;若是使用遞歸,程序可能更容易理解。如何選擇要看什麼對你來講更重要。」

基線條件和遞歸條件

每一個遞歸函數都有兩部分:基線條件(base case)和遞歸條件(recursive case)。遞歸條件指的是函數調用本身,而基線條件則指的是函數再也不調用本身,從而避免造成無限循環。遞歸條件必定是向基線條件靠攏的,不然,只能無限遞歸下去。如

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

上述函數中i的值收斂於0,即達到基線條件,從而不會無限遞歸下去。

主要講了與遞歸相關的一種數據結構--棧(stack)。棧是一種支持FILO(First In Last Out)的數據結構。
爲何要提這種數據結構呢?

調用棧

這是由於現代計算機對函數的實現用到了調用棧(call stack)的棧。調用函數時,計算機將首先爲該函數調用分配一塊內存。每當你調用函數時,計算機都將函數調用涉及的全部變量的值存儲到內存中。

假設A函數調用B函數,此時計算機爲兩個函數分配了內存,暫且稱之爲A函數內存B函數內存,它們的位置關係以下:
----棧頂----
B函數內存
—————
A函數內存
—————

若B函數執行完,計算機就能夠回收B函數內存了,即從棧頂彈出B函數內存,此時只有A函數內存了。
----棧頂----
A函數內存
—————

以上操做符合FILO的定義,調用棧是棧的一種具體應用。

那若是調用棧數量太多,會有什麼後果呢?

遞歸調用棧

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

對於較小的正整數,這個程序沒有問題;而若是x較大,在fact執行的時會發現內存量會飆升,甚至會出現程序沒法正常執行下去。這是由於此時遞歸調用棧的狀況相似:
----棧頂----
fact(1)函數內存
———————
...
———————
fact(n-2)函數內存
———————
fact(n-1)函數內存
———————
fact(n)函數內存
———————

fact(n)依賴fact(n-1),依次類推,致使計算機存儲了大量函數調用的信息。這類問題大致有兩種解決方式:

  1. 從新編寫代碼,轉而使用循環。
  2. 使用尾遞歸。如今已經有不少編譯器支持尾遞歸優化了。

個人公衆號地址
圖片描述

相關文章
相關標籤/搜索