深入理解漢諾塔問題與遞迴
什麼是漢諾塔問題
相傳在古印度聖廟中,有一種被稱為漢諾塔(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
連結: ofollow,noindex" target="_blank">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盤子搬回來 } }
仍然是那句話:對遞迴的理解的要點主要在於放棄!對於遞迴,我們只需掌握層與層之間的聯絡,而究竟怎麼完成,每個元素的作用無需擔心。實際上只要邏輯正確,那麼就不用擔心遞迴的錯誤。
最後,放兩張動圖
引自知乎 醬紫君