基本概念
在定義一個函數時,出現調用自身函數的,稱為遞歸(recursion)。
如果一個遞歸函數,最后一條語句是遞歸調用語句,則稱這種遞歸調用為尾遞歸(tail recursion)。
一個遞歸模型通常有兩部分構成:初值(遞歸出口)和遞歸體。
遞歸的使用條件
遞歸的數學定義,比如斐波那契數列:F(1)=F(2)=1,F(n)=F(n−1)+F(n−2),n≥3F(1)=F(2)=1,F(n)=F(n−1)+F(n−2),n≥3。
遞歸的數據結構,出現了指向自身的指針或者引用,如鏈表、樹、圖等。
遞歸的求解方法。比如經典的漢諾塔問題。
遞歸函數的時間空間
求解遞歸函數的時間通常需要根據問題解出相應的遞歸式。
對於形如歸並排序的分治算法,其遞歸式通常形如T(n)=aT(bn)+f(n)T(n)=aT(bn)+f(n),通常可以使用主定理(《算法導論》 p53)來求解。
一般情況的遞歸算法的時間分析可能比較困難,需要詳細了解遞歸的執行過程。比如動態規划法和暴力算法都可以使用遞歸,但是他們的時間復雜度有顯著差異。
遞歸的空間復雜度除了要考慮分配的臨時變量之外,還需要考慮遞歸的深度(雖然使用的是棧空間,也要將其計算在內。)
遞歸程序非遞歸化
對於遞歸的實現機理,需要理解現代CPU的棧幀模型。棧幀保存了當前函數狀態的相關信息。當函數調用另一個函數時,它將保存臨時變量等信息,同時為被調用的函數開辟另一個幀。因此遞歸函數調用,每一層是不會相互影響的。
通常情況下遞歸是由編譯器自動實現,然而系統的棧空間是固定的,對於遞歸深度較大的情況,可能會出現棧溢出(stack overflow),因此這時候必須用棧的數據結構手動模擬棧幀,來實現遞歸程序非遞歸化。具體操作來說,就是在原來遞歸出現之前,利用棧保存前一層的環境,然后切換到下一層;在原來遞歸返回到調用函數時,將棧頂元素出棧,得到被保存的環境。
一個例子可以參考之前第3章綜合練習的一道題目字符串解碼(Decode String)。
需要注意的是,編譯器在自動實現遞歸的過程中,它能夠自動將傳值的參數進行恢復,但是不能將傳地址(引用)的參數(尤其是定義在全局變量、堆空間的)進行恢復。這種情況下,需要手動將其恢復。這個問題可以思考DFS遍歷迷宮時,用int [][]和vector<vector<int>>的異同:因為vector是一個類對象,每次自我調用的壓棧都會將其復制一份,在返回時出棧也要調用析構函數銷毀。這樣保證了每次使用的vector<int>都是相互獨立的,自然不需要手動恢復。而int [][]則是直接傳地址,在一個地方修改了,其他地方也是修改的。雖然使用vector<vector<int>>能夠省去恢復的麻煩,但是其反復復制元素造成的效率問題也是不容忽視的。
遞歸算法的一般設計步驟
對於一般的遞歸問題,通常需要先轉化成一個包含有初始狀態和遞推狀態的模型。
遞歸一般是將較復雜的大問題,利用遞推關系轉化為一個或者多個相對較小的問題。直到最后每個子問題都滿足初始條件(達到遞歸出口)。
對於遞歸定義問題的求解,直接利用定義進行遞推就可以了。某些較為復雜的,非簡單的數學問題,就需要抽象出遞推關系。
抽象遞推關系也是非常重要的,動態規划算法就是基於遞推關系來確定最優解的。
如何具體設計遞歸算法,包括簡單的回溯法(back-tracking)、動態規划算法(dynamic programming),會在習題里結合具體的例子說明。
---------------------
作者:_g63
來源:CSDN
原文:https://blog.csdn.net/jsxyg63/article/details/78306061
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!