如何通徹的理解遞歸算法?

計算機科學的新學生通常難以理解遞歸程序設計的概念。遞歸思想之所以困難,原因在于它非常像是循環(huán)推理(circular reasoning)。它也不是一個直觀的過程;當我們指揮別人做事的時候,我們極少會遞歸地指揮他們。

Introduction

遞歸算法是一種直接或者間接調用自身函數(shù)或者方法的算法。

遞歸算法的實質是把問題分解成規(guī)模縮小的同類問題的子問題,然后遞歸調用方法來表示問題的解。遞歸算法對解決一大類問題很有效,它可以使算法簡潔和易于理解。

遞歸算法,其實說白了,就是程序的自身調用。它表現(xiàn)在一段程序中往往會遇到調用自身的那樣一種coding策略,這樣我們就可以利用大道至簡的思想,把一個大的復雜的問題層層轉換為一個小的和原問題相似的問題來求解的這樣一種策略。

遞歸往往能給我們帶來非常簡潔非常直觀的代碼形勢,從而使我們的編碼大大簡化,然而遞歸的思維確實跟我們的常規(guī)思維相逆的,我們通常都是從上而下的思維問題, 而遞歸趨勢從下往上的進行思維。這樣我們就能看到我們會用很少的語句解決了非常大的問題,所以遞歸策略的最主要體現(xiàn)就是小的代碼量解決了非常復雜的問題。

遞歸算法解決問題的特點:

? 遞歸就是方法里調用自身。

? 在使用遞增歸策略時,必須有一個明確的遞歸結束條件,稱為遞歸出口。

? 遞歸算法解題通常顯得很簡潔,但遞歸算法解題的運行效率較低。所以一般不提倡用遞歸算法設計程序。

? 在遞歸調用的過程當中系統(tǒng)為每一層的返回點、局部量等開辟了棧來存儲。遞歸次數(shù)過多容易造成棧溢出等,所以一般不提倡用遞歸算法設計程序。

遞歸算法要求。遞歸算法所體現(xiàn)的“重復”一般有三個要求:

(1) 是每次調用在規(guī)模上都有所縮小(通常是減半);

(2) 是相鄰兩次重復之間有緊密的聯(lián)系,前一次要為后一次做準備(通常前一次的輸出就作為后一次的輸入);

(3) 是在問題的規(guī)模極小時必須用直接給出解答而不再進行遞歸調用,因而每次遞歸調用都是有條件的(以規(guī)模未達到直接解答的大小為條件),無條件遞歸調用將會成為死循環(huán)而不能正常結束。

從遞歸的經(jīng)典示例開始

計算階乘

計算階乘是遞歸程序設計的一個經(jīng)典示例。計算某個數(shù)的階乘就是用那個數(shù)去乘包括 1 在內的所有比它小的數(shù)。例如,factorial(5) 等價于 5*4*3*2*1,而 factorial(3) 等價于 3*2*1。

階乘的一個有趣特性是,某個數(shù)的階乘等于起始數(shù)(starting number)乘以比它小1的數(shù)的階乘。例如,factorial(5) 與5 * factorial(4) 相同。您很可能會像這樣編寫階乘函數(shù):

(注:本文的程序示例用C語言編寫)

不過,這個函數(shù)的問題是,它會永遠運行下去,因為它沒有終止的地方。函數(shù)會連續(xù)不斷地調用 factorial。 當計算到零時,沒有條件來停止它,所以它會繼續(xù)調用零和負數(shù)的階乘。因此,我們的函數(shù)需要一個條件,告訴它何時停止。

由于小于 1 的數(shù)的階乘沒有任何意義,所以我們在計算到數(shù)字 1 的時候停止,并返回 1 的階乘(即 1)。因此,真正的遞歸函數(shù)類似于:

精密詳解c/c++遞歸算法,感受遞歸算法的獨特用處

可見,只要初始值大于零,這個函數(shù)就能夠終止。停止的位置稱為 基線條件(base case)。基線條件是遞歸程序的最底層位置,在此位置時沒有必要再進行操作,可以直接返回一個結果。所有遞歸程序都必須至少擁有一個基線條件,而且 必須確保它們最終會達到某個基線條件;否則,程序將永遠運行下去,直到程序缺少內存或者棧空間。

斐波納契數(shù)列

斐波納契數(shù)列(Fibonacci Sequence),最開始用于描述兔子生長的數(shù)目時用上了這數(shù)列。從數(shù)學上,費波那契數(shù)列是以遞歸的方法來定義:

這樣斐波納契數(shù)列的遞歸程序就可以非常清晰的寫出來了:

精密詳解c/c++遞歸算法,感受遞歸算法的獨特用處

遞歸程序的基本步驟

每一個遞歸程序都遵循相同的基本步驟:

(1) 初始化算法。遞歸程序通常需要一個開始時使用的種子值(seed value)。要完成此任務,可以向函數(shù)傳遞參數(shù),或者提供一個入口函數(shù), 這個函數(shù)是非遞歸的,但可以為遞歸計算設置種子值。

(2) 檢查要處理的當前值是否已經(jīng)與基線條件相匹配。如果匹配,則進行處理并返回值。

(3) 使用更小的或更簡單的子問題(或多個子問題)來重新定義答案。

(4) 對子問題運行算法。

(5) 將結果合并入答案的表達式。

(6) 返回結果。

使用歸納定義

有時候,編寫遞歸程序時難以獲得更簡單的子問題。不過,使用歸納定義的(inductively-defined)數(shù)據(jù)集 可以令子問題的獲得更為簡單。歸納定義的數(shù)據(jù)集是根據(jù)自身定義的數(shù)據(jù)結構 —— 這叫做歸納定義(inductive definition)。

例如,鏈表就是根據(jù)其本身定義出來的。鏈表所包含的節(jié)點結構體由兩部分構成:它所持有的數(shù)據(jù),以及指向另一個節(jié)點結構體(或者是 NULL,結束鏈表)的指針。 由于節(jié)點結構體內部包含有一個指向節(jié)點結構體的指針,所以稱之為是歸納定義的。

使用歸納數(shù)據(jù)編寫遞歸過程非常簡單。注意,與我們的遞歸程序非常類似,鏈表的定義也包括一個基線條件 —— 在這里是 NULL 指針。 由于 NULL 指針會結束一個鏈表,所以我們也可以使用 NULL 指針條件作為基于鏈表的很多遞歸程序的基線條件。

下面看兩個例子。

鏈表求和示例

讓我們來看一些基于鏈表的遞歸函數(shù)示例。假定我們有一個數(shù)字列表,并且要將它們加起來。履行遞歸過程序列的每一個步驟,以確定它如何應用于我們的求和函數(shù):

(1) 初始化算法。這個算法的種子值是要處理的第一個節(jié)點,將它作為參數(shù)傳遞給函數(shù)。

(2) 檢查基線條件。程序需要檢查確認當前節(jié)點是否為 NULL 列表。如果是,則返回零,因為一個空列表的所有成員的和為零。

(3) 使用更簡單的子問題重新定義答案。我們可以將答案定義為當前節(jié)點的內容加上列表中其余部分的和。為了確定列表其余部分的和, 我們針對下一個節(jié)點來調用這個函數(shù)。

(4) 合并結果。遞歸調用之后,我們將當前節(jié)點的值加到遞歸調用的結果上。

這樣我們就可以很簡單的寫出鏈表求和的遞歸程序,實例如下:

精密詳解c/c++遞歸算法,感受遞歸算法的獨特用處

漢諾塔問題

漢諾塔(Hanoi Tower)問題也是一個經(jīng)典的遞歸問題,該問題描述如下:

漢諾塔問題:古代有一個梵塔,塔內有三個座A、B、C,A座上有64個盤子,盤子大小不等,大的在下,小的在上(如圖)。有一個和尚想把這64個盤子從A座移到B座,但每次只能允許移動一個盤子,并且在移動過程中,3個座上的盤子始終保持大盤在下,小盤在上。

精密詳解c/c++遞歸算法,感受遞歸算法的獨特用處

Hanoi Tower Solving

? 如果只有 1 個盤子,則不需要利用B塔,直接將盤子從A移動到C。

? 如果有 2 個盤子,可以先將盤子1上的盤子2移動到B;將盤子1移動到C;將盤子2移動到C。這說明了:可以借助B將2個盤子從A移動到C,當然,也可以借助C將2個盤子從A移動到B。

? 如果有3個盤子,那么根據(jù)2個盤子的結論,可以借助c將盤子1上的兩個盤子從A移動到B;將盤子1從A移動到C,A變成空座;借助A座,將B上的兩個盤子移動到C。

以此類推,上述的思路可以一直擴展到 n 個盤子的情況,將將較小的 n-1個盤子看做一個整體,也就是我們要求的子問題,以借助B塔為例,可以借助空塔B將盤子A上面的 n-1 個盤子從A移動到B;將A最大的盤子移動到C,A變成空塔;借助空塔A,將B塔上的 n-2 個盤子移動到A,將C最大的盤子移動到C,B變成空塔…

根據(jù)以上的分析,不難寫出程序:

精密詳解c/c++遞歸算法,感受遞歸算法的獨特用處

將循環(huán)轉化為遞歸

在下表中了解循環(huán)的特性,看它們可以如何與遞歸函數(shù)的特性相對比。

精密詳解c/c++遞歸算法,感受遞歸算法的獨特用處

可見,遞歸函數(shù)與循環(huán)有很多類似之處。實際上,可以認為循環(huán)和遞歸函數(shù)是能夠相互轉換的。 區(qū)別在于,使用遞歸函數(shù)極少被迫修改任何一個變量 —— 只需要將新值作為參數(shù)傳遞給下一次函數(shù)調用。 這就使得您可以獲得避免使用可更新變量的所有益處,同時能夠進行重復的、有狀態(tài)的行為。

下面還是以階乘為例子,循環(huán)寫法為:

精密詳解c/c++遞歸算法,感受遞歸算法的獨特用處

遞歸寫法在第二節(jié)中已經(jīng)介紹過了,這里就不重復了,可以比較一下。

尾遞歸介紹

對于遞歸函數(shù)的使用,人們所關心的一個問題是棧空間的增長。確實,隨著被調用次數(shù)的增加,某些種類的遞歸函數(shù)會線性地增加棧空間的使用 —— 不過,有一類函數(shù),即尾部遞歸函數(shù),不管遞歸有多深,棧的大小都保持不變。尾遞歸屬于線性遞歸,更準確的說是線性遞歸的子集。

函數(shù)所做的最后一件事情是一個函數(shù)調用(遞歸的或者非遞歸的),這被稱為 尾部調用(tail-call)。使用尾部調用的遞歸稱為 尾部遞歸。當編譯器檢測到一個函數(shù)調用是尾遞歸的時候,它就覆蓋當前的活動記錄而不是在棧中去創(chuàng)建一個新的。編譯器可以做到這點,因為遞歸調用是當前活躍期內最后一條待執(zhí)行的語句,于是當這個調用返回時棧幀中并沒有其他事情可做,因此也就沒有保存棧幀的必要了。通過覆蓋當前的棧幀而不是在其之上重新添加一個,這樣所使用的棧空間就大大縮減了,這使得實際的運行效率會變得更高。

讓我們來看一些尾部調用和非尾部調用函數(shù)示例,以了解尾部調用的含義到底是什么:

精密詳解c/c++遞歸算法,感受遞歸算法的獨特用處

可見,要使調用成為真正的尾部調用,在尾部調用函數(shù)返回之前,對其結果 不能執(zhí)行任何其他操作。

注意,由于在函數(shù)中不再做任何事情,那個函數(shù)的實際的棧結構也就不需要了。惟一的問題是,很多程序設計語言和編譯器不知道如何除去沒有用的棧結構。如果我們能找到一個除去這些不需要的棧結構的方法,那么我們的尾部遞歸函數(shù)就可以在固定大小的棧中運行。

在尾部調用之后除去棧結構的方法稱為 尾部調用優(yōu)化 。

那么這種優(yōu)化是什么?我們可以通過詢問其他問題來回答那個問題:

(1) 函數(shù)在尾部被調用之后,還需要使用哪個本地變量?哪個也不需要。

(2) 會對返回的值進行什么處理?什么處理也沒有。

(3) 傳遞到函數(shù)的哪個參數(shù)將會被使用?哪個都沒有。

好像一旦控制權傳遞給了尾部調用的函數(shù),棧中就再也沒有有用的內容了。雖然還占據(jù)著空間,但函數(shù)的棧結構此時實際上已經(jīng)沒有用了,因此,尾部調用優(yōu)化就是要在尾部進行函數(shù)調用時使用下一個棧結構 覆蓋 當前的棧結構,同時保持原來的返回地址。

我們所做的本質上是對棧進行處理。再也不需要活動記錄(activation record),所以我們將刪掉它,并將尾部調用的函數(shù)重定向返回到調用我們的函數(shù)。 這意味著我們必須手工重新編寫棧來仿造一個返回地址,以使得尾部調用的函數(shù)能直接返回到調用它的函數(shù)。

Conclusion

遞歸是一門偉大的藝術,使得程序的正確性更容易確認,而不需要犧牲性能,但這需要程序員以一種新的眼光來研究程序設計。對新程序員來說,命令式程序設計通常是一個更為自然和直觀的起點,這就是為什么大部分程序設計說明都集中關注命令式語言和方法的原因。 不過,隨著程序越來越復雜,遞歸程序設計能夠讓程序員以可維護且邏輯一致的方式更好地組織代碼。

歡迎+q群666295498,一起討論學習 邀請碼啟明學c
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372

推薦閱讀更多精彩內容