學習筆記
學習書目:《算法圖解》- 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變量。