咱們都見識了很多關於遞歸與尾遞歸的各類長篇概論,本文將經過對下面幾個問題的直觀體驗,來幫助加深對遞歸的理解。編程
本文內容目錄:bash
棧 是一種常見的數據結構,具備後進先出(LIFO)的特色。 調用棧 則是計算機內部對函數調用所分配內存時的一種棧結構。數據結構
遞歸函數 簡單的講,就是函數在內部調用本身。編程語言
在編寫遞歸函數的時候,咱們要注意組成它的兩個條件,分別是:基線條件 和 遞歸條件 (也叫回歸條件)。ide
遞歸函數實際上是利用了分而治之的思想(Divide and Conquer D&C),下面用一個簡單的遞歸函數來講明。函數
假設咱們如今須要一個遞增函數increasing(n)
,其實現爲:優化
def increasing(n = 0):
print('n = %d' % n)
increasing(n + 1)
複製代碼
咱們很容易發現,這樣的代碼會永不休止的執行,最後會形成棧溢出,簡單的說就是內存滿了。由於根本沒人告訴它何時該停下來,因此它不斷的重複執行,形成無限循環。ui
假設遞增的值到100的時候就再也不執行,則其實現爲:編碼
def increasing(n = 0):
print('n = %d' % n)
if n == 100: // --> 基線條件
return
else: // --> 遞歸條件
increasing(n + 1)
複製代碼
從上面能夠看出,遞歸條件指的是函數在內部繼續調用本身,基線條件指的是函數再也不調用本身的狀況。spa
所謂 Divide and Conquer,分別對應的則是遞歸條件和基線條件。
下面咱們經過計算一個數的階乘的函數進行解釋。它將會有三個不一樣版本,分別是遞歸求階乘,尾遞歸求階乘,for循環求階乘。
由於這裏要研究遞歸的調用棧狀況,因此咱們先來看看遞歸求階乘的實現:
print('##### 遞歸求階乘 #####')
def fact(n):
if n == 1:
return 1
else:
return n * fact(n - 1)
print('result = %s' % fact(4))
複製代碼
爲了更好的解釋說明,我將上面的代碼略做改動:
print('##### 遞歸求階乘 #####')
def fact(n):
if n == 1:
result = 1
return result
else:
print('current: n = %d, result = %d * fact(%d - 1)' % (n, n, n))
result = fact(n - 1)
return n * result
複製代碼
改動理由:
所謂活躍期,指的是計算機當前所操做的函數執行期。
運行結果爲:
##### 遞歸求階乘 #####
current: n = 4, result = 4 * fact(4 - 1)
current: n = 3, result = 3 * fact(3 - 1)
current: n = 2, result = 2 * fact(2 - 1)
result = 24
複製代碼
其調用棧狀況:
正常狀況下,棧頂函數執行完畢後將彈出。但咱們卻看到遞歸函數的調用不斷的向調用棧壓入執行函數,那麼問題來了,爲何調用棧前面的函數"執行完畢"後不自動彈出呢?
答案是 棧頂函數其實並未執行完成,由於棧頂函數的變量result的值還沒有肯定,它還須要 下一個遞歸函數返回的值(上下文) 來計算,因此一直處於非活躍期狀態被保留在調用棧中。
上面的答案還需完善一下,由於當某個棧頂函數,例如fact(1),在執行到基線條件時,result的值已經肯定下來,而無需等待下一個遞歸函數的上下文,因此該棧頂函數真正執行完畢,並彈出調用棧。又由於下一個棧頂函數能夠拿到已彈棧的函數返回的上下文,於是當彈棧函數交待完成後,也相繼彈出調用棧。
咱們先來看看尾遞歸求階乘的實現:
print('##### 尾遞歸求階乘 #####')
def fact_tail(n):
return tail_fact_count(n)
def tail_fact_count(n, result = 1):
if n == 1:
return result
else:
print('current: n = %d, result = %d' % (n, result))
print('next: n = %d, result = %d' % (n - 1, result * n))
print('----------------')
return tail_fact_count(n - 1, n * result)
print('result = %s' % fact_tail(4))
複製代碼
一樣的,咱們將上述代碼略做改動:
print('##### 尾遞歸求階乘 #####')
def fact_tail(n):
result = tail_fact_count(n)
return result
def tail_fact_count(n, result = 1):
if n == 1:
return result
else:
print('current: n = %d, result = %d' % (n, result))
print('next: n = %d, result = %d' % (n - 1, result * n))
print('----------------')
result = n * result
n = n - 1
return tail_fact_count(n, result)
print('result = %s' % fact_tail(4))
複製代碼
運行結果爲:
##### 尾遞歸求階乘 #####
current: n = 4, result = 1
next: n = 3, result = 4
----------------
current: n = 3, result = 4
next: n = 2, result = 12
----------------
current: n = 2, result = 12
next: n = 1, result = 24
----------------
result = 24
複製代碼
咱們再來看看它的調用棧狀況:
仔細對比前面遞歸函數的調用棧狀況,咱們能夠看出遞歸與尾遞歸調用棧的兩個明顯不一樣點:
咱們再來看看前面遞歸函數的實現。在遞歸實現中,result的值由於須要 下一個遞歸函數返回的值 來計算才能肯定,因此棧頂函數(設A)一直在調用棧中停留等待下一個棧頂函數(設B)的返回值,一旦下一個棧頂函數(B)返回了確切的result值,那麼當B交待完成以後就會彈出,所謂交待便是由於上一個棧頂函數A須要下一個棧頂函數即B的返回值,當A拿到了B的值就是交待完成了。以此類推,遞歸的彈棧順序則如圖所示由下往上彈出。
那麼尾遞歸究竟作了什麼貓膩?
尾遞歸其實在result的值上作了貓膩。在尾遞歸的實現中,result的值在當前棧頂函數中已經肯定下來了,並經計算後交待給下一個棧頂函數。因此當棧頂函數完成了它的使命(把result值傳遞給下一個執行函數),它就會愉快的在調用棧上彈出。
概括來說:
在本例子中的 目的 指的是肯定result值。
按照慣例,先上代碼。可是爲了更好的理解與尾遞歸的聯繫,最好仍是花個十幾秒思考一下如何實現for循環求階乘吧~
爲了減小篇幅,直接貼上略做修改的代碼:
print('##### for循環求階乘 #####')
def fact_for(n):
if n == 1:
return 1
else:
result = 1
for i in range(n, 0, -1):
print('current: n = %d, result = %d' % (i, result))
result = for_fact_count(i, result)
return result
def for_fact_count(n, result = 1):
return n * result
print('result = %s' % fact_for(4))
複製代碼
運行結果爲:
##### for循環求階乘 #####
current: n = 4, result = 1
current: n = 3, result = 4
current: n = 2, result = 12
current: n = 1, result = 24
result = 24
複製代碼
當咱們思考如何使用for循環去實現求階乘的過程當中,咱們會想到用一個變量去存儲計算的值。在上述代碼中指的就是 result (= 1)
。
爲了便於理解for循環與尾遞歸,我設計了這麼一個函數 for_fact_count(n, result = 1)
,它接收 當前result值並經計算後刷新result值。
在不影響for循環的實現我已經將其與尾遞歸的實現作了類似的轉化(連名字的都好類似啦),因此請開始你的表演,把for循環求階乘的調用棧畫出來吧~
- 雖然說本文使用了Python進行編碼解釋,可是目前大多數編程語言都沒有針對尾遞歸作優化,Python解釋器也沒有,因此即使使用了尾遞歸進行求階乘,在運行過程當中仍是會形成棧溢出。而Xcode在debug環境下不會對尾遞歸作優化,需將其設爲release。
- 小生才疏淺陋,文中不免有錯漏之處,請多多指教,感謝您的閱讀。