遞歸函數


如果一個函數在內部調用自身,這個函數就叫做遞歸函數

遞歸函數的簡單定義如下:

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': {},
            },
            '上地': {
                '百度': {},
            },
        },
        '昌平': {
            '沙河': {
                '老男孩': {},
                '北航': {},
            },
            '天通苑': {},
            '回龍觀': {},
        },
        '朝陽': {},
        '東城': {},
    },
    '上海': {
        '閔行': {
            "人民廣場": {
                '炸雞店': {}
            }
        },
        '閘北': {
            '火車戰': {
                '攜程': {}
            }
        },
        '浦東': {},
    },
    '山東': {},
}
menu
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))

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM