1. 程式人生 > 其它 >深入理解遞迴

深入理解遞迴

深入理解遞迴

​ 我們再寫程式時經常會使用遞迴函式來解決問題,但是對於很多人來說,遞迴是比較難以理解的,或者說,很多人對於遞迴的理解是不夠深入的,而如果在對遞迴的理解不深入的情況下就貿然地使用遞迴的話,往往就會使我們程式出現各種意想不到bug,而且往往也難以排錯。而且有很多經典地演算法也涉及到遞迴。因此深入理解遞迴對我們IT人員來說至關重要,本篇部落格將會詳細地講解關於遞迴的內容。

首先是關於遞迴的定義:在程式語言中,我們將呼叫自身的函式稱作遞迴函式,即定義一個函式,在這個函式的可執行程式碼中再呼叫自己本身函式。

其次,也是最重要的,關於遞迴函式的基本原理:遞迴函式是用棧結構來實現的,遞迴的整個過程就是一個入棧和出棧的過程,當程式執行一個函式時就會建立一個新的受保護的獨立空間(新函式棧)。在遞迴函式執行時,由上一層進入下一層函式時,上一層會暫停執行,並且會暫停區域性變數的值、暫存器中儲存的資料等,各層之間的區域性變數是相互獨立、互不影響的。

然後,關於遞迴的幾個基本規則:1.遞迴函式必須設定函數出口(結束條件),並且函式的執行方向必須朝著滿足出口條件的方向進行,否則就會無限遞迴,造成棧溢位。2.當下一層的函式執行完畢後上一層的函式才會執行,所以只有當最後一層函式執行完畢後,每層函式才會由內向外依次執行,當最外層函式執行完畢後,整個遞迴函式才算執行完畢。

接下來,用幾個具體的例子來理解遞迴:

1.求n!

def fn(n):
    if n ==1 or n == 0:
        return 1
    else:
        return n*fn(n-1)

print(fn(5))
#120

這個函式的執行情況如圖

上面的題很簡單,下面用一個較為複雜的題

2.迴路計數問題:

下面用遞迴的方式來解這道題,算然該方法並不是最優解,但是通過追蹤步驟可以讓我們更加清晰地認識遞迴。

import math
import sys
sys.setrecursionlimit(10000)
n = int(input())
li = [i for i in range(2, n + 1)]
def dfs(li, li_tmp):
    global count
    if len(li_tmp) == len(li):
        count += 1
        return
    for j in range(len(li)):
        tmp = li_tmp[:]  # 進行淺拷貝,不然後面回不去
        if li[j] not in li_tmp and math.gcd(li_tmp[-1], li[j]) == 1:
            li_tmp.append(li[j])
            dfs(li, li_tmp)
            li_tmp = tmp[:]
count = 0
for i in range(len(li)):
    li_tmp = [li[i]]
    dfs(li, li_tmp)
print(count)

我們以n=5為例,分析當li_tmp = li[0] 即li_tmp = [2]時,函式的遞迴過程

下面是除錯的具體過程:

​ 一開始,當li = [2,3,4,5]和li_tmp = [2]傳進去時,會先進行if判斷,注意已經執行完的部分在以後的遞迴中不會再執行,因為上一層程式碼再次執行是執行後面沒有執行完的程式碼,執行完if判斷之後進入for迴圈,當遍歷到的值滿足條件時,就會進入下一層函式,這裡要注意的是如果此時的for迴圈沒有執行完,那麼當從內向外再次返回執行時,就會接著後面的內容執行,沒有執行完的迴圈也會再次執行,直到迴圈結束。就這樣一層一層地執行,直到li_tmp = [2,3,4,5],最後一層函式判斷if len(li_tmp) == len(li) 滿足條件,執行return,函式執行完畢,這時候,函式就會返回到上一層,而上一層函式n=3,for迴圈內執行到的dfs(li, li_tmp)執行完畢,此時li_tmp = [2,3,4,5],然後接著向下走li_tmp = tmp[:],li_tmp變為[2,3,4],然後返回到for迴圈頂部,發現不滿足條件退出迴圈,然後要知道,在這一層li_tmp = [2,3,4,5],所以li_tmp 又變為了[2,3,4,5],上面我們說過上層函式暫停時會暫停區域性變數的值、暫存器中儲存的資料等,各層之間互不影響。至此,上層函式執行完畢,然後再進入上上層,因為上上層的i=2,它的for迴圈還沒有到頭,所以它會再次進行for迴圈,並執行for迴圈裡面的程式碼,從而再次進行遞迴。。。。後面的內容就大同小異了。

​ 通過分析上訴例子,我們可以得知,當上層函式暫停執行時,函式內部的變數會儲存當時的值,而且其中的控制語句如for迴圈等也會暫停執行等待迴歸重啟。