python基礎(補充):正經人誰用遞歸呀


在講解遞歸函數之前我們先了解一下棧堆

單獨講棧堆是數據結構

  • 棧:后進先出的一種數據結構
  • 堆:排序后的一種樹狀數據結構

棧區堆區是內存空間

  • 棧區:按照后進先出的數據結構(棧),無論創建或銷毀都是自動為數據分配內存,釋放內存(系統自動做的)
  • 堆區:按照排序后的樹狀數據結構(堆),可優先取出必要數據,無論創建或銷毀都是手動分配內存,釋放內存(程序員手動做的)
  • 內存中的棧區: 自動分配 自動釋放
  • 內存中的堆區: 手動分配 手動釋放

運行程序時在內存中執行,會因為數據類型的不同而在內存的不同區域運行,因不同語言對內存划分的機制不一,但大體來講,有如下四大區域:

  • 棧區:分配局部變量空間。
  • 堆區:是用於手動分配程序員申請的內存空間。
  • 靜態區(全局棧區):分配靜態變量,全局變量空間。
  • 代碼區(只讀區,常量區):分配常量和程序代碼空間的。

棧區 堆區 靜態區 代碼區 都是內存中的一段空間

遞歸函數

在函數內部,可以調用其他函數。如果一個函數在內部調用自身本身,這個函數就是遞歸函數。

舉個例子,我們來計算階乘n! = 1 x 2 x 3 x ... x n,用函數func(n)表示,可以看出:

func(n)=n!=1×2×3×⋅⋅⋅×(n−1)×n=(n−1)!×n=func(n−1)×n

所以,func(n)可以表示為n x func(n-1),只有n=1時需要特殊處理。

於是,func(n)用遞歸的方式寫出來就是:

def func(n):
    if n==1:
        return 1
    return n * func(n - 1)

上面就是一個遞歸函數。可以試試:

print(func(1))
# 1

print(func(5))
# 120

print(func(100))
# 93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

如果我們計算func(5),可以根據函數定義看到計算過程如下:

遞歸函數的優點是定義簡單,邏輯清晰。理論上,所有的遞歸函數都可以寫成循環的方式,但循環的邏輯不如遞歸清晰。

使用遞歸函數需要注意防止棧溢出。在計算機中,函數調用是通過棧(stack)這種數據結構實現的,每當進入一個函數調用,棧就會加一層棧幀,每當函數返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞歸調用的次數過多,會導致棧溢出。可以試試func(1000)

print(func(1000))
'''
Traceback (most recent call last):
  File "D:/python_project/mxxl/test/test.py", line 7, in <module>
    print(func(1000))
  File "D:/python_project/mxxl/test/test.py", line 4, in func
    return n * func(n - 1)
  File "D:/python_project/mxxl/test/test.py", line 4, in func
    return n * func(n - 1)
  File "D:/python_project/mxxl/test/test.py", line 4, in func
    return n * func(n - 1)
  [Previous line repeated 995 more times]
  File "D:/python_project/mxxl/test/test.py", line 2, in func
    if n==1:
RecursionError: maximum recursion depth exceeded in comparison
'''

解決遞歸調用棧溢出的方法是通過尾遞歸優化。

尾遞歸

事實上尾遞歸和循環的效果是一樣的,所以,把循環看成是一種特殊的尾遞歸函數也是可以的。

尾遞歸是指,在函數返回的時候,調用自身本身,並且,return語句不能包含表達式。

這樣,編譯器或者解釋器就可以把尾遞歸做優化,使遞歸本身無論調用多少次,都只占用一個棧幀,不會出現棧溢出的情況。

上面的func(n)函數由於return n * func(n - 1)引入了乘法表達式,所以就不是尾遞歸了。要改成尾遞歸方式,需要多一點代碼,主要是要把每一步的乘積傳入到遞歸函數中:

def func(n):
    return func_iter(n, 1)

def func_iter(num, product):
    if num == 1:
        return product
    return func_iter(num - 1, num * product)

可以看到,return func_iter(num - 1, num * product)僅返回遞歸函數本身,num - 1num * product在函數調用前就會被計算,不影響函數調用。

func(5)對應的func_iter(5, 1)的調用如下:

尾遞歸調用時,如果做了優化,棧不會增長,因此,無論多少次調用也不會導致棧溢出。

遺憾的是,大多數編程語言沒有針對尾遞歸做優化,Python解釋器也沒有做優化,所以,即使把上面的func(n)函數改成尾遞歸方式,也會導致棧溢出。


免責聲明!

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



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