1. 程式人生 > 其它 >資料結構和演算法--遞迴和尾遞迴

資料結構和演算法--遞迴和尾遞迴

遞迴和尾遞迴

遞迴

1、定義:

  • 子問題必須和原始問題相同,且更為簡單;
  • 不能無限制的呼叫本身,必須有個出口,化簡為非遞迴狀況處理。

2、場景:

# 遞迴實現
def fact(n: int):
    """
    求n!
    :param n:
    :return:
    """
    if n < 0:
        return 0
    elif n == 0 or n == 1:
        return  1
    else:
        return n * fact(n - 1)

3、原理分析:

在每次函式呼叫計算n倍的(n-1)!的值,讓n=n-1並持續這個過程直到n=1為止。這種定義不是尾遞迴的,
因為每次函式呼叫的返回值都依賴於用n乘以下一次函式呼叫的返回值,因此每次呼叫產生的棧幀將不得不儲存在棧上直到下一個子呼叫的返回值確定。

尾遞迴

1、定義:

  • 如何一個函式中所有遞迴形式呼叫都出現在函式末尾, 稱這個遞迴函式是尾遞迴。

  • 遞迴呼叫是整個函式體中最後一個執行語句且它返回值不屬於表示式一部分;

  • 迴歸過程中不用做任何操作(大多數程式碼編譯器會利用這種特性自動生成優化程式碼);

2、尾遞迴原理:

當編譯器檢測到一個函式是尾遞迴時,他就覆蓋當前活動記錄, 而不是在棧中建立一個新的
因為遞迴呼叫是當前活躍期內最後一條執行的語句,於是當這個呼叫返回時,棧中並沒有其他事情可做,因此沒有儲存棧幀
的必要。通過覆蓋當前棧幀而不是在其之上重新新增一個,這樣使用棧空間大大縮減,實際執行效率變高。

eg:
尾遞迴階乘實現:

def fact_tail(n: int, res: int):
    """
    尾遞迴方式,求n!
    :param n:
    :param res:
    :return:
    """
    if n < 0:
        return 0
    elif n == 0:
        return 1
    elif n == 1:
        return res
    else:
        return fact_tail(n - 1, n * res)

尾遞迴解析:

函式比程式碼1多個引數res,除此之外並沒有太大區別。res(初始化為1)維護遞迴層次的深度。這就讓我們避免了每次還需要將返回值再乘以n。
然而,在每次遞迴呼叫中,令res=n*res並且n=n-1。繼續遞迴呼叫,直到n=1,這滿足結束條件,此時直接返回res即可。

總結:
遞迴和尾遞迴的不同:
示例中的函式是尾遞迴的,因為對facttail的單次遞迴呼叫是函式返回前最後執行的一條語句。
換句話說,在遞迴呼叫之後還可以有其他的語句執行,只是它們只能在遞迴呼叫沒有執行時才可以執行。

尾遞迴是極其重要的,不用尾遞迴,函式的堆疊耗用難以估量,需要儲存很多中間函式的堆疊。
比如sum(n) = f(n) = f(n-1) + value(n) ;
會儲存n個函式呼叫堆疊,而使用尾遞迴f(n, sum) = f(n-1, sum+value(n)); 這樣則只保留後一個函式堆疊即可,之前的可優化刪去。

優化尾遞迴的裝飾器

有一個針對尾遞迴優化的decorator