如果一個函數在內部調用自身,這個函數就叫做遞歸函數
遞歸函數的簡單定義如下:
def recursion(): return recursion()
這只是一個簡單的定義,什么也做不了。
當然,你可以嘗試會發生什么結果,理論上會永遠運行下去,但實際操作時發現不一會兒程序就報錯了,因為每次調用函數都會用掉一點內存,在足夠多的函數調用發生后,空間幾乎被占滿,程序就會報錯。
RecursionError: maximum recursion depth exceeded
#超過最大遞歸深度
這類遞歸被稱為無窮遞歸(infinite recursion),理論上永遠都不會結束,當然,我們需要能實際做事情的函數,有用的遞歸函數應該滿足如下條件:
(1)當函數直接返回值時有基本實例(最小可能性問題)
(2)遞歸實例,包括一個或多個問題最小部分的遞歸調用
使用遞歸的關鍵在於將問題分解為小部分,遞歸不能永遠進行下去,因為它總是以最小可能性問題結束,而這些問題又存儲在基本實例中。
函數調用自身怎么實現呢??
其實函數每次被調用時都會創建一個新的命名空間,也就是當函數調用‘自己’時,實際上運行的是兩個不同的函數(也可以說一個函數具有兩個函數的命名空間)。
我們來看一個遞歸示例,計算階乘n!=1*2*3*4*5*……*n,用函數fact(n)表示,可以看出:
fact(n)=1*2*3*……*(n-1)*n=(n-1)!*n=fact(n-1)*n
所以,fact(n)可以表示為n*fact(n-1),只有n=1時需要特殊處理
於是,fact(n)用遞歸方式定義函數如下:
def fact(n): if n==1: return 1 return n*fact(n-1)
執行該函數:
>>> fact(1) 1 >>> fact(5) 120 >>> fact(100) 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000L
如果我們計算fact(5),可以根據函數定義看到計算過程如下:
===> fact(5) ===> 5 * fact(4) ===> 5 * (4 * fact(3)) ===> 5 * (4 * (3 * fact(2))) ===> 5 * (4 * (3 * (2 * fact(1)))) ===> 5 * (4 * (3 * (2 * 1))) ===> 5 * (4 * (3 * 2)) ===> 5 * (4 * 6) ===> 5 * 24 ===> 120
由函數定義可知,遞歸函數的有點是定義簡單,邏輯清晰。
理論上,所有遞歸函數都可以寫成循環的方式,不過循環的邏輯不如遞歸清晰。
使用遞歸函數需要注意仿制棧溢出,在計算機中,函數調用通過棧(stack)這種數據結構實現的。每當進入一個函數調用,棧就會增加一層棧幀,每當函數返回,棧就會減一層棧幀,憂郁棧的大小不是無線的,因此遞歸調用的次數過多會導致棧溢出。可以試試fact(1000),執行結果如下:
RecursionError: maximum recursion depth exceeded in comparison
由執行結果看到,執行出現異常,異常提示超過最大遞歸深度。
那么怎么解決這個問題呢?
首先我們可以通過修改最大遞歸深度來增加遞歸深度。通過sys模塊的setrecursionlimit()方法來修改。
import sys sys.setrecursionlimit(2000)#這樣就可以增加最大遞歸深度
但是這樣也治標不治本,如果遞歸次數太多,那就說明這個問題就不適合用遞歸來解決。
還有一種方法,就是通過尾遞歸優化,事實上尾遞歸和循環的效果一樣,把循環看成一種特殊尾遞歸函數也是可以的。
尾遞歸是指在函數返回時只能調用函數本身,return語句不能包含表達式,這樣,編譯器或解釋器就可以對尾遞歸進行優化,使遞歸本身無論調用多少次都只占用一個棧幀,從而避免棧溢出的情況。
由於上面的fact(n)函數return n*(n-1)引入了乘法表達式,因此不是尾遞歸,要改成尾遞歸方式需要多一點代碼,主要是把每一步乘積傳入遞歸函數(通過把乘積結果傳入函數參數的方式),看如下函數定義方式:
def fact(n,ret=1): if n==0: return ret return fact(n-1,ret=ret*n) print(fact(5))#輸出120
可以看到return fact(n-1,ret=ret*n)僅返回函數本身,n-1和ret=ret*n在函數調用前就會被計算,不影響函數的調用。
尾遞歸調用時,如果做了優化,棧不會增長,因此,無論多少次調用也不會導致棧溢出。
遺憾的是,大多數編程語言沒有針對尾遞歸做優化,Python解釋器也沒有做優化,所以,即使把上面的fact(n)函數改成尾遞歸方式,也會導致棧溢出。
def fact(n,ret=1): if n==0: return ret return fact(n-1,ret=ret*n) print(fact(1000)) #輸出 RecursionError: maximum recursion depth exceeded in comparison
但是可以通過裝飾器方法手動進行尾遞歸優化,這里暫不敘述,詳細方法百度
遞歸與三級菜單:
menu = { '北京': { '海淀': { '五道口': { 'soho': {}, '網易': {}, 'google': {} }, '中關村': { '愛奇藝': {}, '汽車之家': {}, 'youku': {}, }, '上地': { '百度': {}, }, }, '昌平': { '沙河': { '老男孩': {}, '北航': {}, }, '天通苑': {}, '回龍觀': {}, }, '朝陽': {}, '東城': {}, }, '上海': { '閔行': { "人民廣場": { '炸雞店': {} } }, '閘北': { '火車戰': { '攜程': {} } }, '浦東': {}, }, '山東': {}, }
def threeLM(dic): while True: for k in dic:print(k) key = input('input>>').strip() if key == 'b' or key == 'q':return key elif key in dic.keys() and dic[key]: ret = threeLM(dic[key]) if ret == 'q': return 'q' threeLM(menu) #會迷惑的一個地方就是當輸入b和q時,返回key(此時key=b或q)是返回給上一次return的地方,
l = [menu] while l: for key in l[-1]:print(key) k = input('input>>').strip() # 北京 if k in l[-1].keys() and l[-1][k]:l.append(l[-1][k]) elif k == 'b':l.pop() elif k == 'q':break
二分查找法
lis=[2,3,5,10,15,16,18,22,26,30,32,35,41,42,43,55,56,66,67,69,72,76,82,83,88] def find(aim,li,start=0,end=None): end=len(li) if end is None else end middle=(end-start)//2+start if start<=end: if li[middle]>aim: return find(aim,li,start=start,end=middle-1) elif li[middle]<aim: return find(aim,li,start=middle+1,end=end) else: return middle else: return 'not find' print(find(16,lis))
