寫(xiě)遞歸函數(shù)的正確思維方法無(wú)標(biāo)題文章

遞歸是編程中一個(gè)相對(duì)難以理解但是卻又很重要的概念. 對(duì)于從命令式語(yǔ)言開(kāi)始學(xué)習(xí)編程的程序員天生對(duì)此有理解缺陷, 而對(duì)于從類似C++這種對(duì)函數(shù)式編程范式不友好的語(yǔ)言開(kāi)始學(xué)習(xí)編程的程序員就更加如此了.(比如我自己) 碰巧(其實(shí)不巧)最近在讀這本書(shū)(這本書(shū)國(guó)內(nèi)沒(méi)有引進(jìn), 網(wǎng)上只有巨貴的亞馬遜賣的原版, 我讀的是網(wǎng)上的中文版), Paul Graham在書(shū)中講述的如何寫(xiě)遞歸函數(shù)的部分, 讓我印象深刻. 因?yàn)樵瓡?shū)是講Lisp的, 當(dāng)然這個(gè)部分也是用Lisp作為例子描述的, 考慮到國(guó)內(nèi)會(huì)看這本書(shū)的人太少, 能看懂Lisp的就更不多了, 我這里根據(jù)自己的理解, 重新整理一下. 最重要的是, 書(shū)中原來(lái)的例子太少, 太簡(jiǎn)單, 我自己提供了一些額外的, 并且更加復(fù)雜的例子. 以期對(duì)問(wèn)題能有更好的理解.

什么是遞歸

理解遞歸

使用遞歸

遞歸的問(wèn)題

參考

什么是遞歸

迭代的是人,遞歸的是神

–L. Peter Deutsch

簡(jiǎn)單的定義: “當(dāng)函數(shù)直接或者間接調(diào)用自己時(shí)竖般,則發(fā)生了遞歸.” 說(shuō)起來(lái)簡(jiǎn)單, 但是理解起來(lái)復(fù)雜, 因?yàn)檫f歸并不直觀, 也不符合我們的思維習(xí)慣, 相對(duì)于遞歸, 我們更加容易理解迭代. 因?yàn)槲覀內(nèi)粘I钪械乃季S方式就是一步接一步的, 并且能夠理解一件事情做了N遍這個(gè)概念. 而我們?nèi)粘I钪袔缀醪粫?huì)有遞歸思維的出現(xiàn).

舉個(gè)簡(jiǎn)單的例子, 即在C/C++中計(jì)算一個(gè)字符串的長(zhǎng)度. 下面是傳統(tǒng)的方式, 我們一般都這樣通過(guò)迭代來(lái)計(jì)算長(zhǎng)度, 也很好理解.

size_tlength(constchar*str){size_tlength=0;while(*str!=0){++length;++str;}returnlength;}

而事實(shí)上, 我們也可以通過(guò)遞歸來(lái)完成這樣的任務(wù).

size_tlength(constchar*str){if(*str==0){return0;}returnlength(++str)+1;}

只不過(guò), 我們都不這么做罷了, 雖然這樣的實(shí)現(xiàn)有的時(shí)候可能代碼更短, 但是很明顯, 從思維上來(lái)說(shuō)更加難以理解一些. 當(dāng)然, 我是說(shuō)假如你不是習(xí)慣于函數(shù)式語(yǔ)言的話. 這個(gè)例子相對(duì)簡(jiǎn)單, 稍微看一下還是能明白吧.

迭代的算法可以這樣描述: 從第一個(gè)字符開(kāi)始判斷字符串的每一個(gè)字符, 當(dāng)該字符不為0的時(shí)候, 該字符串的長(zhǎng)度加一.

遞歸的算法可以這樣描述: 當(dāng)前字符串的長(zhǎng)度等于當(dāng)前字符串除了首字符后, 剩下的字符串長(zhǎng)度+1.

作為這么簡(jiǎn)單的例子, 兩種算法其實(shí)大同小異, 雖然我們習(xí)慣迭代, 但是, 也能看到, 遞歸的算法無(wú)論是從描述上還是實(shí)際實(shí)現(xiàn)上, 并不比迭代要麻煩.

理解遞歸

在初學(xué)遞歸的時(shí)候, 看到一個(gè)遞歸實(shí)現(xiàn), 我們總是難免陷入不停的回溯驗(yàn)證之中, 因?yàn)榛厮菥拖穹催^(guò)來(lái)思考迭代, 這是我們習(xí)慣的思維方式, 但是實(shí)際上遞歸不需要這樣來(lái)驗(yàn)證. 比如, 另外一個(gè)常見(jiàn)的例子是階乘的計(jì)算. 階乘的定義: “一個(gè)正整數(shù)的階乘(英語(yǔ):factorial)是所有小于或等于該數(shù)的正整數(shù)的積搂捧,并且0的階乘為1冲泥』巢矗” 以下是Ruby的實(shí)現(xiàn):

deffactorial(n)ifn<=1thenreturn1elsereturnn*factorial(n-1)endend

我們?cè)趺磁袛噙@個(gè)階乘的遞歸計(jì)算是否是正確的呢? 先別說(shuō)測(cè)試, 我說(shuō)我們讀代碼的時(shí)候怎么判斷呢?

回溯的思考方式是這么驗(yàn)證的, 比如當(dāng)n = 4時(shí), 那么factoria(4)等于4 * factoria(3), 而factoria(3)等于3 * factoria(2),factoria(2)等于2 * factoria(1), 等于2 * 1, 所以factoria(4)等于4 * 3 * 2 * 1. 這個(gè)結(jié)果正好等于階乘4的迭代定義.

用回溯的方式思考雖然可以驗(yàn)證當(dāng)n = 某個(gè)較小數(shù)值是否正確, 但是其實(shí)無(wú)益于理解.

Paul Graham提到一種方法, 給我很大啟發(fā), 該方法如下:

當(dāng)n=0, 1的時(shí)候, 結(jié)果正確.

假設(shè)函數(shù)對(duì)于n是正確的, 函數(shù)對(duì)n+1結(jié)果也正確.

如果這兩點(diǎn)是成立的本砰,我們知道這個(gè)函數(shù)對(duì)于所有可能的n都是正確的蜒蕾。

這種方法很像數(shù)學(xué)歸納法, 也是遞歸正確的思考方式, 事實(shí)上, 階乘的遞歸表達(dá)方式就是1!=1及汉,n!=(n-1)!×n(見(jiàn)wiki). 當(dāng)程序?qū)崿F(xiàn)符合算法描述的時(shí)候, 程序自然對(duì)了, 假如還不對(duì), 那是算法本身錯(cuò)了…… 相對(duì)來(lái)說(shuō), n,n+1的情況為通用情況, 雖然比較復(fù)雜, 但是還能理解, 最重要的, 也是最容易被新手忽略的問(wèn)題在于第1點(diǎn), 也就是基本用例(base case)要對(duì). 比如, 上例中, 我們?nèi)サ鬷f n <= 1的判斷后, 代碼會(huì)進(jìn)入死循環(huán), 永遠(yuǎn)不會(huì)結(jié)束.

使用遞歸

既然遞歸比迭代要難以理解, 為啥我們還需要遞歸呢? 從上面的例子來(lái)看, 自然意義不大, 但是很多東西的確用遞歸思維會(huì)更加簡(jiǎn)單……

經(jīng)典的例子就是斐波那契數(shù)列, 在數(shù)學(xué)上, 斐波那契數(shù)列就是用遞歸來(lái)定義的:

·F0 = 0

·F1 = 1

·Fn = Fn – 1 + Fn – 2

有了遞歸的算法, 用程序?qū)崿F(xiàn)實(shí)在再簡(jiǎn)單不過(guò)了:

deffibonacci(n)ifn==0thenreturn0elsifn==1thenreturn1elsereturnfibonacci(n-1)+fibonacci(n-2)endend

改為用迭代實(shí)現(xiàn)呢? 你可以試試.

上面講了怎么理解遞歸是正確的, 同時(shí)可以看到在有遞歸算法描述后, 其實(shí)程序很容易寫(xiě), 那么最關(guān)鍵的問(wèn)題就是, 我們?cè)趺凑业揭粋€(gè)問(wèn)題的遞歸算法呢?

Paul Graham提到, 你只需要做兩件事情:

你必須要示范如何解決問(wèn)題的一般情況, 通過(guò)將問(wèn)題切分成有限小并更小的子問(wèn)題.

你必須要示范如何通過(guò)有限的步驟, 來(lái)解決最小的問(wèn)題(基本用例).

如果這兩件事完成了, 那問(wèn)題就解決了. 因?yàn)檫f歸每次都將問(wèn)題變得更小, 而一個(gè)有限的問(wèn)題終究會(huì)被解決的, 而最小的問(wèn)題僅需幾個(gè)有限的步驟就能解決.

這個(gè)過(guò)程還是數(shù)學(xué)歸納法的方法, 只不過(guò)和上面提到的一個(gè)是驗(yàn)證, 一個(gè)是證明.

現(xiàn)在我們用這個(gè)方法來(lái)尋找漢諾塔這個(gè)游戲的解決方法.(這其實(shí)是數(shù)學(xué)家發(fā)明的游戲)

有三根桿子A沮趣,B,C坷随。A桿上有N個(gè)(N>1)穿孔圓盤房铭,盤的尺寸由下到上依次變小驻龟。要求按下列規(guī)則將所有圓盤移至C桿:

1.每次只能移動(dòng)一個(gè)圓盤.

2.大盤不能疊在小盤上面.


這個(gè)游戲在只有3個(gè)盤的時(shí)候玩起來(lái)較為簡(jiǎn)單, 盤越多, 就越難, 玩進(jìn)去后, 你就會(huì)進(jìn)入一種不停的通過(guò)回溯來(lái)推導(dǎo)下一步該干什么的狀態(tài), 這是比較難的. 我記得第一次碰到這個(gè)游戲好像是在大航海時(shí)代某一代游戲里面, 當(dāng)時(shí)就覺(jué)得挺有意思的. 推薦大家都實(shí)際的玩一下這個(gè)游戲, 試試你腦袋能想清楚幾個(gè)盤的情況.

現(xiàn)在我們來(lái)應(yīng)用Paul Graham的方法思考這個(gè)游戲.

一般情況:

當(dāng)有N個(gè)圓盤在A上, 我們已經(jīng)找到辦法將其移到C杠上了, 我們?cè)趺匆苿?dòng)N+1個(gè)圓盤到C杠上呢? 很簡(jiǎn)單, 我們首先用將N個(gè)圓盤移動(dòng)到C上的方法將N個(gè)圓盤都移動(dòng)到B上, 然后再把第N+1個(gè)圓盤(最后一個(gè))移動(dòng)到C上, 再用同樣的方法將在B杠上的N個(gè)圓盤移動(dòng)到C上. 問(wèn)題解決.

基本用例:

當(dāng)有1個(gè)圓盤在A上, 我們直接把圓盤移動(dòng)到C上即可.

算法描述大概就是上面這樣了, 其實(shí)也可以看作思維的過(guò)程, 相對(duì)來(lái)說(shuō)還是比較自然的. 下面是Ruby解:

defhanoi(n,from,to,other)ifn==1thenputsfrom+' -> '+toelsehanoi(n-1,from,other,to)hanoi(1,from,to,other)hanoi(n-1,other,to,from)endend

當(dāng)n=3時(shí)的輸出:

A -> C

A -> B

C -> B

A -> C

B -> A

B -> C

A -> C

上述代碼中, from, to, other的作用其實(shí)也就是提供一個(gè)桿子的替代符, 在n=1時(shí), 其實(shí)也就相當(dāng)于直接移動(dòng). 看起來(lái)這么復(fù)雜的問(wèn)題, 其實(shí)用遞歸這么容易, 沒(méi)有想到吧. 要是想用迭代來(lái)解決這個(gè)問(wèn)題呢? 還是你自己試試吧, 你試的越多, 就能越體會(huì)到遞歸的好處.

遞歸的問(wèn)題

當(dāng)然, 這個(gè)世界上沒(méi)有啥時(shí)萬(wàn)能的, 遞歸也不例外, 首先遞歸并不一定適用所有情況, 很多情況用迭代遠(yuǎn)遠(yuǎn)比用遞歸好了解, 其次, 相對(duì)來(lái)說(shuō), 遞歸的效率往往要低于迭代的實(shí)現(xiàn), 同時(shí), 內(nèi)存好用也會(huì)更大, 雖然這個(gè)時(shí)候可以用尾遞歸來(lái)優(yōu)化, 但是尾遞歸并不是一定能簡(jiǎn)單做到.

參考

Ansi Common Lisp

精通遞歸程序設(shè)計(jì)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市缸匪,隨后出現(xiàn)的幾起案子翁狐,更是在濱河造成了極大的恐慌,老刑警劉巖凌蔬,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件露懒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡砂心,警方通過(guò)查閱死者的電腦和手機(jī)懈词,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)辩诞,“玉大人坎弯,你說(shuō)我怎么就攤上這事≡甑梗” “怎么了荞怒?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵洒琢,是天一觀的道長(zhǎng)秧秉。 經(jīng)常有香客問(wèn)我,道長(zhǎng)衰抑,這世上最難降的妖魔是什么象迎? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮呛踊,結(jié)果婚禮上砾淌,老公的妹妹穿的比我還像新娘。我一直安慰自己谭网,他們只是感情好汪厨,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著愉择,像睡著了一般劫乱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锥涕,一...
    開(kāi)封第一講書(shū)人閱讀 51,443評(píng)論 1 302
  • 那天衷戈,我揣著相機(jī)與錄音,去河邊找鬼层坠。 笑死殖妇,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的破花。 我是一名探鬼主播谦趣,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼疲吸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了前鹅?” 一聲冷哼從身側(cè)響起磅氨,我...
    開(kāi)封第一講書(shū)人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嫡纠,沒(méi)想到半個(gè)月后烦租,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡除盏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年叉橱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片者蠕。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窃祝,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出踱侣,到底是詐尸還是另有隱情粪小,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布抡句,位于F島的核電站探膊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏待榔。R本人自食惡果不足惜逞壁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锐锣。 院中可真熱鬧腌闯,春花似錦、人聲如沸雕憔。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)斤彼。三九已至分瘦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間畅卓,已是汗流浹背擅腰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留翁潘,地道東北人趁冈。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親渗勘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子沐绒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容