1. 程式人生 > >理解遞迴思想

理解遞迴思想

什麼是遞迴

遞迴(Recursion),指在函式的定義中使用函式自身的方法,即程式的自身呼叫。

遞迴一詞還較常用於描述以自相似方法重複事物的過程。例如,當兩面鏡子相互之間近似平行時,鏡中巢狀的影象是以無限遞迴的形式出現的。也可以理解為自我複製的過程。<!--more-->

遞迴演算法的特點

  • 遞迴就是方法裡呼叫自身。

  • 出口:在使用遞增歸策略時,必須有一個明確的遞迴結束條件,稱為遞迴出口。

  • 效率:遞迴演算法解題通常顯得很簡潔,但遞迴演算法解題的執行效率較低。所以一般不提倡用遞迴演算法設計程式。

  • 棧溢位:在遞迴呼叫的過程當中系統為每一層的返回點、區域性量等開闢了棧來儲存。遞迴次數過多容易造成棧溢位等,所以一般不提倡用遞迴演算法設計程式。

遞迴程式的基本步驟

1.初始化演算法。遞迴程式通常需要一個開始時使用的種子值(seed value)。要完成此任務,可以向函式傳遞引數,或者提供一個入口函式, 這個函式是非遞迴的,但可以為遞迴計算設定種子值。1

2.檢查要處理的當前值是否已經與基線條件相匹配。如果匹配,則進行處理並返回值。

3.使用更小的或更簡單的子問題(或多個子問題)來重新定義答案。

4.對子問題執行演算法。

5.將結果合併入答案的表示式。

6.返回結果。總結流程:初始化——檢查當前值與基線條件的匹配——化小重定義——對子問題執行演算法——結果歸總——返回結果

遞迴與迴圈的比較

PropertiesLoopsRecursive functions
重複為了獲得結果,反覆執行同一程式碼塊;以完成程式碼塊或者執行 continue 命令訊號而實現重複執行。為了獲得結果,反覆執行同一程式碼塊;以反覆呼叫自己為訊號而實現重複執行。
終止條件為了確保能夠終止,迴圈必須要有一個或多個能夠使其終止的條件,而且必須保證它能在某種情況下滿足這些條件的其中之一。為了確保能夠終止,遞迴函式需要有一個基線條件,令函式停止遞迴。
狀態迴圈進行時更新當前狀態。當前狀態作為引數傳遞。

例子

計算階乘n! = 1 x 2 x 3 x ... x n,用函式fact(n)表示:

  def fact(n):
      if n == 1:
          return 1
  return n * fact(n - 1)

尾遞迴--解決棧溢位

棧溢位:使用遞迴函式需要注意防止棧溢位。在計算機中,函式呼叫是通過棧(stack)這種資料結構實現的,每當進入一個函式呼叫,棧就會加一層棧幀,每當函式返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞迴呼叫的次數過多,會導致棧溢位。

尾遞迴(tail-call)優化:在尾部進行函式呼叫時使用下一個棧結構覆蓋當前棧結構,同時保持原來的返回地址。

本質是對棧進行處理,刪掉活動記錄(activation record),在函式返回的時候,呼叫自身本身,並且return語句不能包含表示式。這樣,編譯器或者直譯器就可以把尾遞迴做優化,使遞迴本身無論呼叫多少次,都只佔用一個棧幀,不會出現棧溢位的情況。

要使呼叫成為真正的尾部呼叫,在尾部呼叫函式返回前,對其結果不能執行任何其他操作。

不管遞迴有多深,棧大小保持不變。尾遞歸屬於線性遞迴的子集。用尾遞迴優化改造上面的階乘演算法,主要是要把每一步的乘積傳入到遞迴函式中:

  def fact(n):
      return fact_iter(n, 1)
  
  def fact_iter(num, product):
      if num == 1:
          return product
      return fact_iter(num - 1, num * product)

可以看到,return fact_iter(num - 1, num * product)僅返回遞迴函式本身,num - 1和num * product在函式呼叫前就會被計算,不影響函式呼叫。可以看到,return fact_iter(num - 1, num * product)僅返回遞迴函式本身,num - 1和num * product在函式呼叫前就會被計算,不影響函式呼叫。

尾遞迴事實上和迴圈是等價的,沒有迴圈語句的程式語言只能通過尾遞迴實現迴圈。

btw:歡迎關注 ~

Github: https://github.com/ScarlettYellow

個人部落格:https://scarletthuang.cn/