理解遞迴思想
什麼是遞迴
遞迴(Recursion),指在函式的定義中使用函式自身的方法,即程式的自身呼叫。
遞迴一詞還較常用於描述以自相似方法重複事物的過程。例如,當兩面鏡子相互之間近似平行時,鏡中巢狀的影象是以無限遞迴的形式出現的。也可以理解為自我複製的過程。<!--more-->
遞迴演算法的特點
遞迴就是方法裡呼叫自身。
出口:在使用遞增歸策略時,必須有一個明確的遞迴結束條件,稱為遞迴出口。
效率:遞迴演算法解題通常顯得很簡潔,但遞迴演算法解題的執行效率較低。所以一般不提倡用遞迴演算法設計程式。
棧溢位:在遞迴呼叫的過程當中系統為每一層的返回點、區域性量等開闢了棧來儲存。遞迴次數過多容易造成棧溢位等,所以一般不提倡用遞迴演算法設計程式。
遞迴程式的基本步驟
1.初始化演算法。遞迴程式通常需要一個開始時使用的種子值(seed value)。要完成此任務,可以向函式傳遞引數,或者提供一個入口函式, 這個函式是非遞迴的,但可以為遞迴計算設定種子值。1
2.檢查要處理的當前值是否已經與基線條件相匹配。如果匹配,則進行處理並返回值。
3.使用更小的或更簡單的子問題(或多個子問題)來重新定義答案。
4.對子問題執行演算法。
5.將結果合併入答案的表示式。
6.返回結果。總結流程:初始化——檢查當前值與基線條件的匹配——化小重定義——對子問題執行演算法——結果歸總——返回結果
遞迴與迴圈的比較
Properties | Loops | Recursive functions |
---|---|---|
重複 | 為了獲得結果,反覆執行同一程式碼塊;以完成程式碼塊或者執行 continue 命令訊號而實現重複執行。 | 為了獲得結果,反覆執行同一程式碼塊;以反覆呼叫自己為訊號而實現重複執行。 |
終止條件 | 為了確保能夠終止,迴圈必須要有一個或多個能夠使其終止的條件,而且必須保證它能在某種情況下滿足這些條件的其中之一。 | 為了確保能夠終止,遞迴函式需要有一個基線條件,令函式停止遞迴。 |
狀態 | 迴圈進行時更新當前狀態。 | 當前狀態作為引數傳遞。 |
例子
計算階乘n! = 1 x 2 x 3 x ... x n,用函式fact(n)表示:
def fact(n): if n == 1: return 1return n * fact(n - 1)
尾遞迴--解決棧溢位
棧溢位:使用遞迴函式需要注意防止棧溢位。在計算機中,函式呼叫是通過棧(stack)這種資料結構實現的,每當進入一個函式呼叫,棧就會加一層棧幀,每當函式返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞迴呼叫的次數過多,會導致棧溢位。
尾遞迴(tail-call)優化:在尾部進行函式呼叫時使用下一個棧結構覆蓋當前棧結構,同時保持原來的返回地址。
本質是對棧進行處理,刪掉活動記錄(activation record),在函式返回的時候,呼叫自身本身,並且return語句不能包含表示式。這樣,編譯器或者直譯器就可以把尾遞迴做優化,使遞迴本身無論呼叫多少次,都只佔用一個棧幀,不會出現棧溢位的情況。
要使呼叫成為真正的尾部呼叫,在尾部呼叫函式返回前,對其結果不能執行任何其他操作。
不管遞迴有多深,棧大小保持不變。尾遞歸屬於線性遞迴的子集。用尾遞迴優化改造上面的階乘演算法,主要是要把每一步的乘積傳入到遞迴函式中:
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在函式呼叫前就會被計算,不影響函式呼叫。可以看到,return fact_iter(num - 1, num * product)僅返回遞迴函式本身,num - 1和num * product在函式呼叫前就會被計算,不影響函式呼叫。
尾遞迴事實上和迴圈是等價的,沒有迴圈語句的程式語言只能通過尾遞迴實現迴圈。
btw:歡迎關注 ~
Github: https://github.com/ScarlettYellow
個人部落格:https://scarletthuang.cn/