什么是漢諾塔問題
相傳在古印度聖廟中,有一種被稱為漢諾塔(Hanoi)的游戲。該游戲是在一塊銅板裝置上,有三根桿(編號A、B、C),在A桿自下而上、由大到小按順序放置64個金盤(如下圖)。游戲的目標:把A桿上的金盤全部移到C桿上,並仍保持原有順序疊好。操作規則:每次只能移動一個盤子,並且在移動過程中三根桿上都始終保持大盤在下,小盤在上,操作過程中盤子可以置於A、B、C任一桿上。
我們先來放一段代碼:
#include<stdio.h> int move(int plate_n,char from,char to,char buffer) { if(plate_n==1) printf("%c-->%c\n",from,to); else { move(plate_n-1,from,buffer,to); printf("%c-->%c\n",from,to); move(plate_n-1,buffer,to,from); } } int main(void) { int n; printf("Input step:"); scanf("%d",&n); move(n,'a','b','c'); return 0; }
對於這段代碼的解析,一般網上的解釋是這樣的:漢諾塔的算法就3個步驟:第一,把a上的n-1個盤通過c移動到b。第二,把a上的最下面的盤移到c。第三,因為n-1個盤全在b上了,所以把b當做a重復以上步驟就好了。
如果你身為初學者,到此為止,便看懂了以上代碼的內容,那么顯然你的理解力很強。或者你還不懂,那么我們再來看看。
·對遞歸的理解在於放棄
放棄你對於理解和跟蹤遞歸全程的企圖,只理解遞歸兩層之間的交接,以及遞歸終結的條件。
我們先來看一個故事
引自知乎 Fireman A
鏈接:https://www.zhihu.com/question/24385418/answer/257751077想象你來到某個熱帶叢林,意外發現了十層之高的漢諾塔。正當你苦苦思索如何搬動它時,林中出來一個土著,毛遂自薦要幫你搬塔。他名叫二傻,戴着一個草帽,草帽上有一個2字,號稱會把一到二號盤搬到任意柱。
你靈機一動,問道:“你該不會有個兄弟叫三傻吧?”“對對,老爺你咋知道的?他會搬一到三號盤。“”那你去把他叫來,我不需要你了。“於是三傻來了,他也帶着個草帽,上面有個3字。你說:”三傻,你幫我把頭三個盤子移到c柱吧。“三傻沉吟了一會,走進樹林,你聽見他大叫:”二傻,出來幫我把頭兩個盤子搬到C!“
由於天氣炎熱你開始打瞌睡。朦朧中你沒看見二傻是怎么工作的,二傻干完以后,走入林中大叫一聲:“老三,我干完了!”三傻出來,把三號盤從A搬到B,然后又去叫二傻:“老二,幫我把頭兩個盤子搬回A!”余下的我就不多說了,總之三傻其實只搬三號盤,其他叫二傻出來干。最后一步是三傻把三號盤搬到C,然后呼叫二傻來把頭兩個盤子搬回C事情完了之后你把三傻叫來,對他說:“其實你不知道怎么具體一步一步把三個盤子搬到C,是吧?”三傻不解地說:“我不是把任務干完了?”你說:“可你其實叫你兄弟二傻干了大部分工作呀?”三傻說:“我外包給他和你屁相干?”你問到:“二傻是不是也外包給了誰?“三傻笑了:“這跟我有屁相干?”
你苦苦思索了一夜,第二天,你走入林中大叫:“十傻,你在哪?”一個頭上帶着10號草帽的人,十傻,應聲而出:“老爺,你有什么事?”“我要你幫把1到10號盤子搬到C柱““好的,老爺。“十傻轉身就向林內走。“慢着,你該不是回去叫你兄弟九傻吧““老爺你怎么知道的?““所以你使喚他把頭九個盤子搬過來搬過去,你只要搬幾次十號盤就好了,對嗎?““對呀!““你知不知道他是怎么干的?““這和我有屁相干?“你嘆了一口氣,決定放棄。十傻開始干活。樹林里充滿了此起彼伏的叫聲:“九傻,來一下!“ “老八,到你了!““五傻!。。。“”三傻!。。。“”大傻!“你注意到大傻從不叫人,但是大傻的工作也最簡單,他只是把一號盤搬來搬去。
若干年后,工作結束了。十傻來到你面前。你問十傻:“是誰教給你們這么干活的?“十傻說:“我爸爸。他給我留了這張紙條。”
他從口袋里掏出一張小紙條,上面寫着:“照你帽子的號碼搬盤子到目標柱。如果有盤子壓住你,叫你上面一位哥哥把他搬走。如果有盤子占住你要去的柱子,叫你哥哥把它搬到不礙事的地方。等你的盤子搬到了目標,叫你哥哥把該壓在你上面的盤子搬回到你上頭。“
你不解地問:“那大傻沒有哥哥怎么辦?“十傻笑了:“他只管一號盤,所以永遠不會碰到那兩個‘如果’,也沒有盤子該壓在一號上啊。”但這時他忽然變了顏色,好像泄漏了巨大的機密。他驚慌地看了你一眼,飛快地逃入樹林。
第二天,你到樹林里去搜尋這十兄弟。他們已經不知去向。你找到了一個小屋,只容一個人居住,但是屋里有十頂草帽,寫着一到十號的號碼。
PS:這真是一個很有意思的故事…
那么回歸正題,我們開始編寫我們的程序
·首先我們需要一個函數,他的作用是將n個盤子從一個柱子搬到另一個柱子。而通過分析我們又可以知道,這三個柱子又有這樣的特點:
1.一個是盤子所在的柱子,所以我們設為from
2.一個是要目標柱,我們設為to
3.一個可以讓我們中轉使得我們可以從from搬到to去的柱子,我們設為buffer
綜上我們的遞歸函數設為:
int move(int plate_n,char from,char to,char buffer);
接下來便是完成這個遞歸函數
首先我們知道,如果盤子只有一個,那么很簡單,我們直接從from搬到to去就行了,所以我們在第一行寫下:
if(plate_n==1) printf("%c-->%c\n",from,to);
然后便是盤子不為1的情況了,我們首先將n個盤子分為兩部分:底座與n-1組成的上半部分,這個時候我們有這樣的解決方法:
1.我們先把n-1搬到緩沖區去
2.我們把底座搬到目標柱去
3.我們把n-1搬回來
所以這個時候我們該這么寫:
move(plate_n-1,from,buffer,to); //把n-1搬到緩沖區去 printf("%c-->%c\n",from,to); //把底座搬到目標去 move(plate_n-1,buffer,to,from); //把緩沖區的n-1柱搬到目標柱去
綜上,一切結束。放棄你對過程的掌控,放棄你對細節的思考,對於遞歸你應該思考的只是整體,僅此而已。
完成的代碼:
int move(int plate_n,char from,char to,char buffer) { if(plate_n==1) printf("%c-->%c\n",from,to);//只有一個盤子,直接搬 else { move(plate_n-1,from,buffer,to);//將n-1個盤子搬到緩沖區 printf("%c-->%c\n",from,to);//將底盤搬到目標柱 move(plate_n-1,buffer,to,from);//將n-1盤子搬回來 } }
仍然是那句話:對遞歸的理解的要點主要在於放棄!對於遞歸,我們只需掌握層與層之間的聯系,而究竟怎么完成,每個元素的作用無需擔心。實際上只要邏輯正確,那么就不用擔心遞歸的錯誤。
最后,放兩張動圖
引自知乎 醬紫君