函數調用另外一個函數是合法的;函數調用自己也是合法的。調用自己的過程稱爲遞歸函數,這個執行過程叫做遞歸。
遞歸在數據結構中經常會用到,特別是解決樹的遞歸問題時很好用。但是想明白遞歸是挺燒腦的,一般即使兩層、三層遞歸也會容易給人繞進去。要是我們瞭解函數在底層的存儲機制,利用棧(先進後出)來進行分析,或許就容易多了。
不講廢話,直接撈乾的,我們首先回憶下遞歸的規則,
函數遞歸調用的重要規則
-
程序執行一個函數時,就創建一個新的受保護的獨立空間(新函數棧)
-
函數的局部變量是獨立的,不會相互影響
-
遞歸必須向退出遞歸的條件逼近,否則就是無限遞歸,死龜了:)
-
當一個函數執行完畢,或者遇到 return,就會返回,遵守誰調用,就將結果返回給誰。
分析遞歸流程
我們一個例子來快速熟悉遞歸:
#求函數值:
已知 f(1)=3; f(n) = 2*f(n-1)+1;
請使用遞歸的思想編程,求出f(n)的值.
編寫程序
# coding=utf-8
# -*- coding:utf-8 -*-
'''
Author: Solorzhou
Email: [email protected]
date: 2019/10/26 20:13
desc:
'''
def f(n):
if n == 1:
return 3
else:
return 2 * f(n - 1) + 1
print("n=", n)
if __name__ == '__main__':
f(4)
遞歸函數的棧圖
可以看到,一個函數每次被調用時,都會在內存中創建一個幀,來包含函數的局部變量和參數,對於遞歸函數,棧上可能同時存在多個函數幀。當每調用一次函數f(n)時,棧頂指針就會往棧頂移動一個位置,直到滿足退出遞歸的條件(n==1).之後再依次返回當前的結果直接,棧頂指針往下移動。
遞歸,也就是先“遞”後“歸”。
本題則是:
f(4)=2 * f(3) + 1
f(4)=2 * (2 * f(2) + 1) + 1
f(4)=2 * (2 * (2 * f(1) + 1) + 1) + 1
f(4)=2 * (2 * (2 * 3 + 1) + 1) + 1
f(4)=2 * 15 + 1
f(4)=31
一些例子
- 1.斐波那契數列
請使用遞歸的方式,求出斐波那契數 1,1,2,3,5,8,13…
給你一個整數 n,求出它的斐波那契數是多少?
- 2 猴子吃桃子問題
有一堆桃子,猴子第一天吃了其中的一半,並再多吃了一個!以後每天猴子都吃其中的一半,然後再
多吃一個。當到第十天時,想再吃時(還沒吃),發現只有 1 個桃子了。問題:最初共多少個桃子?
這些遞歸問題,當我們使用棧圖來思考時就容易多了。
堅定信念
跟蹤程序執行的流程是閱讀程序的一個方法,但是這樣很快就會陷入到迷宮境況。之前在一本書中看到作者對於遞歸給出了一種“堅定信念”的思想。在遇到一個程序時,不去跟蹤執行的流程,而是假定函數都是正確工作的,都能夠返回正確的結果。
事實上,在使用內置函數時,我們已經在嘗試這樣堅持信念了。如:當調用math.cos
或者math.exp
時,我們並不會去檢查函數的內部實現。因爲我們假定他是正確的,因爲些內置函數的程序員肯定都很優秀。
當調用自己寫的函數時,假定它也是正確的,不需要檢查他的執行流程。看一個關於斐波那契數列的例子:
fibonacci(0) = 0
fibonacci(1) = 1
fibonacci(2) = fibonacci(0) +fibonacci(1)
翻譯成python語言,看起來是這樣:
def fibonacci(n):
if n ==0:
return 0
elif n ==1:
return 1
else:
return fibonacci(n-1) + fibonacci(n-2)
假定寫的程序是正確的,那麼很明顯第三個數等於前兩個數字之和。以上程序運行後肯定也是正確的。
從一名不羈的碼農開始,談風月之餘談技術