遞迴函數

標籤: ,

該主題包含 0 則回覆,有 1 個參與人,並且由  BettyBDChiang6 年, 11 月 前 最後更新。

正在檢視 1 篇文章 - 1 至 1 (共計 1 篇)
  • 作者
    文章
  • #1178

    BettyBDChiang
    參與者

    轉載自廖雪峰的官方網站

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

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

    所以,fact(n)可以表示為n x 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)
    93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000

    如果我們計算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):

    >>> fact(1000)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 4, in fact
      ...
      File "<stdin>", line 4, in fact
    RuntimeError: maximum recursion depth exceeded in comparison
    

    解決遞迴呼叫堆疊溢出的方法是通過尾遞迴優化,事實上尾遞迴和迴圈的效果是一樣的,所以,把迴圈看成是一種特殊的尾遞迴函數也是可以的。

    尾遞迴是指,在函數返回的時候,調用自身本身,並且,return語句不能包含運算式。這樣,編譯器或者解譯器就可以把尾遞迴做優化,使遞迴本身無論調用多少次,都只佔用一個堆疊幀,不會出現堆疊溢出的情況。

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

    def fact(n):
        return fact_iter(n, 1)
    
    def fact_iter(num, product):
        if num == 1:
            return product
        return fact_iter(num - 1, num * product)
    

    可以看到,return fact_iter(num – 1, num * product)僅返回遞迴函數本身,num – 1和num * product在函式呼叫前就會被計算,不影響函式呼叫。
    fact(5)對應的fact_iter(5, 1)的調用如下:

    ===> fact_iter(5, 1)
    ===> fact_iter(4, 5)
    ===> fact_iter(3, 20)
    ===> fact_iter(2, 60)
    ===> fact_iter(1, 120)
    ===> 120
    

    尾遞迴呼叫時,如果做了優化,堆疊不會增長,因此,無論多少次調用也不會導致堆疊溢出。
    遺憾的是,大多數程式設計語言沒有針對尾遞迴做優化,Python解譯器也沒有做優化,所以,即使把上面的fact(n)函數改成尾遞迴方式,也會導致堆疊溢出。

    小結

    使用遞迴函數的優點是邏輯簡單清晰,缺點是過深的調用會導致堆疊溢出。
    針對尾遞迴優化的語言可以通過尾遞迴防止堆疊溢出。尾遞迴事實上和迴圈是等價的,沒有迴圈語句的程式設計語言只能通過尾遞迴實現迴圈。
    Python標準的解譯器沒有針對尾遞迴做優化,任何遞迴函數都存在堆疊溢出的問題。

正在檢視 1 篇文章 - 1 至 1 (共計 1 篇)

抱歉,回覆主題必需先登入。