什么是汉诺塔问题
相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如下图)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
我们先来放一段代码:
#include
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重复以上步骤就好了。
如果你身为初学者,到此为止,便看懂了以上代码的内容,那么显然你的理解力很强。或者你还不懂,那么我们再来看看。
·对递归的理解在于放弃
放弃你对于理解和跟踪递归全程的企图,只理解递归两层之间的交接,以及递归终结的条件。
我们先来看一个故事
想象你来到某个热带丛林,意外发现了十层之高的汉诺塔。正当你苦苦思索如何搬动它时,林中出来一个土著,毛遂自荐要帮你搬塔。他名叫二傻,戴着一个草帽,草帽上有一个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盘子搬回来
}
}
仍然是那句话:对递归的理解的要点主要在于放弃!对于递归,我们只需掌握层与层之间的联系,而究竟怎么完成,每个元素的作用无需担心。实际上只要逻辑正确,那么就不用担心递归的错误。
最后,放两张动图
引自知乎 酱紫君