小白的算法初識課堂(part3)--遞歸

學習筆記
學習書目:《算法圖解》- Aditya Bhargava




遞歸


首先,我們看一段代碼:

def print_num(my_list):
    for i in my_list:
        print(i)

print_num([1, 3, 5, 7, 9])

輸出:

1
3
5
7
9

再看一段代碼:

def print_num2(my_list):
    if my_list:
        print(my_list.pop(0))
        print_num2(my_list)
        
print_num2([1, 3, 5, 7, 9])

輸出:

1
3
5
7
9

我們看到的第一段代碼使用的是循環,第二段代碼使用的是遞歸,兩種方法結果相同。一般來說,遞歸能讓解決方案更清晰(雖然我舉的例子好像沒體現出來遞歸法更清晰),但並沒有性能上的優勢。實際上,在有些情況下,使用循環的性能更好。

如果使用循環,程序的性能可能更高;如果使用遞歸,程序可能更容易理解。如何選擇要看什麼對你來說更重要。


基線條件和遞歸條件


由於遞歸函數調用自己,因此編寫這樣的函數時很容易出錯,進而導致無限循環。例如,假設我要編寫一個像下面這樣倒計時的函數:

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

如果我們運行上述代碼,將發現一個問題:這個函數運行起來沒完沒了!

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

我們來給countdown函數添加一個基線條件:

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

現在,這個函數就會像預期那樣運行.



假設我們有一疊便條,這疊便條記錄着我們馬上要做的待辦事項,我們簡稱這疊便條爲清單。當我們插入的待辦事項時,這個事件會放在清單的最上面;當我們讀取待辦事項時,也只讀取清單最上面的那個,且讀完就將其銷燬。因此這個清單隻有兩種操作:壓入(插入)和彈出(刪除並讀取)。

在這裏插入圖片描述

這種數據結構被稱爲棧。


調用棧


計算機在內部使用被稱爲調用棧的棧。

爲了演示計算機是如何調用棧的,我們來看下面這個簡單的函數:

def greet(name):
    print(name, '!')
    greet2(name)
    print('too late!')
    bye()

def greet2(name):
    print(name, '?')

def bye():
    print('bye!')

greet('maggie')

注意!print是一個函數,但是出於簡化考慮,我們假設它不是函數。


假設,我們調用greet('maggie'),計算機將首先爲該函數調用分配一塊內存空間:

在這裏插入圖片描述


變量name被賦值爲maggie,這需要存儲到內存中:

在這裏插入圖片描述


當我們調用函數時,計算機會將函數調用涉及的所有變量的值存儲到內存中。接下來,我們再調用greet2('maggie').同樣,計算機也爲這個函數調用分配一塊內存。

在這裏插入圖片描述


計算機使用一個棧來表示這些內存塊,其中第二個內存塊位於第一個內存塊上面。我們打印maggie ?,然後從函數greet2的調用返回。此時,棧頂的內存塊被彈出。

在這裏插入圖片描述


現在,棧頂的內存塊是函數greet的,這意味着我們返回到了函數greet。當我調用函數greet2時,函數greet只執行了一部分。調用另一個函數時,當前函數暫停並處於未完成狀態,該函數的所有變量的值仍在內存中。

當執行完greet2函數後,我們繼續向下執行,首先打印too late!,再調用函數bye()。計算機在棧頂添加了函數bye的內存塊,然後我們打印bye!,並從該函數中返回。

在這裏插入圖片描述


現在,我們又回到了greet函數,由於無事可做,我們就從greet函數中返回。這個棧用於存儲多個函數的變量,故被稱爲調用棧。

在這裏插入圖片描述


遞歸調用棧

遞歸函數也使用調用棧,我們來看看下面這個遞歸函數fact:

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

print(fact(3)) 

輸出:

6

下面我們來看一下調用fact(3)時,調用棧的變化:

在這裏插入圖片描述


在這裏插入圖片描述


在這裏插入圖片描述


每個fact調用都有自己的x變量,在一個函數調用中不能訪問另一個函數的x變量。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章