1. 程式人生 > >Python 遞迴函式

Python 遞迴函式

遞迴函式

在函式內部,可以呼叫其他函式。如果一個函式在內部呼叫自身本身,這個函式就是遞迴函式。

遞迴函式特性:

  1. 必須有一個明確的結束條件;
  2. 每次進入更深一層遞迴時,問題規模相比上次遞迴都應有所減少
  3. 相鄰兩次重複之間有緊密的聯絡,前一次要為後一次做準備(通常前一次的輸出就作為後一次的輸入)。
  4. 遞迴效率不高,遞迴層次過多會導致棧溢位(在計算機中,函式呼叫是通過棧(stack)這種資料結構實現的,每當進入一個函式呼叫,棧就會加一層棧幀,每當函式返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞迴呼叫的次數過多,會導致棧溢位)

先舉個簡單的例子:計算1到100之間相加之和;通過迴圈和遞迴兩種方式實現

# 迴圈方式 
def sum_cycle(n): 
    sum = 0 
    for i in range(1,n+1) : 
        sum += i print(sum)

# 遞迴方式 
def sum_recu(n): 
    if n>0: 
       return n +sum_recu(n-1) 
    else: 
       return 0 

sum_cycle(100) 
sum = sum_recu(100) print(sum)

結果:

5050 5050

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

***使用遞迴函式需要注意防止棧溢位。在計算機中,函式呼叫是通過棧(stack)這種資料結構實現的,每當進入一個函式呼叫,棧就會加一層棧幀,每當函式返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞迴呼叫的次數過多,會導致棧溢位。

把上面的遞迴求和函式的引數改成10000就導致棧溢位!

RecursionError: maximum recursion depth exceeded in comparison

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

一般遞迴

def normal_recursion(n):
    if n == 1:
        return 1
    else:
        return n + normal_recursion(n-1)

執行:

normal_recursion(5)
5 + normal_recursion(4)
5 + 4 + normal_recursion(3)
5 + 4 + 3 + normal_recursion(2)
5 + 4 + 3 + 2 + normal_recursion(1)
5 + 4 + 3 + 3
5 + 4 + 6
5 + 10
15

可以看到, 一般遞迴, 每一級遞迴都需要呼叫函式, 會建立新的棧,隨著遞迴深度的增加, 建立的棧越來越多, 造成爆棧:boom:

尾遞迴基於函式的尾呼叫, 每一級呼叫直接返回函式的返回值更新呼叫棧,而不用建立新的呼叫棧, 類似迭代的實現, 時間和空間上均優化了一般遞迴!

def tail_recursion(n, total=0):
    if n == 0:
        return total
    else:
        return tail_recursion(n-1, total+n)

執行:

tail_recursion(5)
tail_recursion(4, 5)
tail_recursion(3, 9)
tail_recursion(2, 12)
tail_recursion(1, 14)
tail_recursion(0, 15)
15

可以看到, 每一級遞迴的函式呼叫變成"線性"的形式.

深入理解尾遞迴

呃, 所以呢? 是不是感覺還不夠過癮... 誰說尾遞迴呼叫就不用建立新的棧呢?

還是讓我們去底層一探究竟吧

int tail_recursion(int n, int total) {
    if (n == 0) {
        return total;
    }
    else {
        return tail_recursion(n-1, total+n);
    }
}

int main(void) {
    int total = 0, n = 4;
    tail_recursion(n, total);
    return 0;
}

反彙編

  • $ gcc -S tail_recursion.c -o normal_recursion.S

  • $ gcc -S -O2 tail_recursion.c -o tail_recursion.S gcc開啟尾遞迴優化

對比反彙編程式碼如下(AT&T語法)

可以看到, 開啟尾遞迴優化前, 使用call呼叫函式, 建立了新的呼叫棧(LBB0_3);

而開啟尾遞迴優化後, 就沒有新的呼叫棧生成了, 而是直接pop

bp指向的 _tail_recursion 函式的地址(pushq %rbp)然後返回,

仍舊用的是同一個呼叫棧!

存在的問題

雖然尾遞迴優化很好, 但python 不支援尾遞迴,遞迴深度超過1000時會報錯

一個牛人想出的解決辦法

實現一個 tail_call_optimized 裝飾器

#!/usr/bin/env python2.4
# This program shows off a python decorator(
# which implements tail call optimization. It
# does this by throwing an exception if it is
# it's own grandparent, and catching such
# exceptions to recall the stack.

import sys

class TailRecurseException:
    def __init__(self, args, kwargs):
        self.args = args
        self.kwargs = kwargs

def tail_call_optimized(g):
    """
    This function decorates a function with tail call
    optimization. It does this by throwing an exception
    if it is it's own grandparent, and catching such
    exceptions to fake the tail call optimization.

    This function fails if the decorated
    function recurses in a non-tail context.
    """
    def func(*args, **kwargs):
        f = sys._getframe()
        # 為什麼是grandparent, 函式預設的第一層遞迴是父呼叫,
        # 對於尾遞迴, 不希望產生新的函式呼叫(即:祖父呼叫),
        # 所以這裡丟擲異常, 拿到引數, 退出被修飾函式的遞迴呼叫棧!(後面有動圖分析)
        if f.f_back and f.f_back.f_back \
            and f.f_back.f_back.f_code == f.f_code:
            # 丟擲異常
            raise TailRecurseException(args, kwargs)
        else:
            while 1:
                try:
                    return g(*args, **kwargs)
                except TailRecurseException, e:
                    # 捕獲異常, 拿到引數, 退出被修飾函式的遞迴呼叫棧
                    args = e.args
                    kwargs = e.kwargs
    func.__doc__ = g.__doc__
    return func

@tail_call_optimized
def factorial(n, acc=1):
    "calculate a factorial"
    if n == 0:
        return acc
    return factorial(n-1, n*acc)

print factorial(10000)

為了更清晰的展示開啟尾遞迴優化前、後呼叫棧的變化和tail_call_optimized裝飾器拋異常退出遞迴呼叫棧的作用, 我這裡利用 pudb除錯工具 做了動圖 <br/>

開啟尾遞迴優化前的呼叫棧

開啟尾遞迴優化後(tail_call_optimized裝飾器)的呼叫棧

通過pudb右邊欄的stack, 可以很清晰的看到呼叫棧的變化.

因為尾遞迴沒有呼叫棧的巢狀, 所以Python也不會報 RuntimeError: maximum recursion depth exceeded 錯誤了!

這裡解釋一下 sys._getframe() 函式:

sys._getframe([depth]):
Return a frame object from the call stack.
If optional integer depth is given, return the frame object that many calls below the top of the stack.
If that is deeper than the call stack, ValueEfror is raised. The default for depth is zero,
returning the frame at the top of the call stack.

即返回depth深度呼叫的棧幀物件.

import sys

def get_cur_info():
    print sys._getframe().f_code.co_filename  # 當前檔名
    print sys._getframe().f_code.co_name  # 當前函式名
    print sys._getframe().f_lineno # 當前行號
    print sys._getframe().f_back # 呼叫者的幀