这篇文章起因是看网易云课堂http://study.163.com/curricula/cs.htm?utm_source=qq&utm_medium=social里一门课提到的汉诺塔问题。
这问题乍看之下很好理解(算法),但是实际写代码的时候总觉得有点疙瘩,感觉不通,这篇文章就是来好好梳理一下。
先贴个代码图:
虽然表面上看,只要解决n个移到b处可以化简为n-1个移动的问题,从而递归解决,但是实际上“化简为n-1个移动的问题”只是递归步骤的一部分,后续的递归步骤和递归基础却没说:
N个从a移动到b的汉诺塔:
递归基础:1.一次只能移动一块
2.大的在小的下面
递归步骤:1.把n-1个从a处移动到c处
1.1把n-2个从a处移动到c处
1.2把n-3个从a处移动到c处
………(迭代过程,一直到最基础的1时再返回去)
2.把最后一块从a处移动到b处
3.把n-1个从c处移动到b处
3.1把n-2个从a处移动到c处
3.2把最后一块从a处移动到b处
………(迭代过程,一直到最基础的1时再返回去)
这样一分析,就发现图中的程序也是这样构造的:这里的函数Hanoi其实就是递归步骤里的1、2、3,而1和3又都调用了Hanoi。
似乎解决了?并没有,里面还有很多的细节没有通,上面的终究只是大方向上的,细节上究竟还有些什么呢?
没有考虑顺序——这样执行为什么不会出现不符合“金字塔“形的结果,即大的在小的上面?
考虑这个问题可能有马后炮的嫌疑,因为一开始我自己是没想通的,在看到实现的方法后通过反推找到了一些联系,姑且先写下来,以后这种思考多了再看看有没有什么共性的东西存在:
递归基础决定了递归步骤——如果没有规定大的必须在小的下面,那么递归步骤几乎不用写了,太简单了,一个个的直接移到b处就好了,而一但加上了这个规定,那么就必须考虑这样带来的限制了:
设定一个模型,如下图
然后我们来看看n个盘时的步骤:
n=4 n=3 n=2
这样很难看出什么规律,只能大概看出来,每种情况都是分三大步,先把n-1个放到c,然后把最大一个放到b,最后把那n-1个放到b,于是有:
n=4时,1~7是第一步,8是第二步,9~15是最后一步;
n=3时,1~3是第一步,4是第二步,5~7是最后一步;
n=2时,1是第一步,2是第二步,3是最后一步。
n=1时,没有那三步,直接是一种新的方式:a直接移动到b即可
我们用流程图的形式再把n=1、2、3、4的过程再写一遍(一行算一步)
N=1时: N=2时: N=3时: n=4时:
a-》b a-》c a-》b——a-》c——b-》c a-》c——a-》b——c-》b——a-》c——b-》a——b-》c——a-》c
a-》b a-》b a-》b
c-》b c-》a——c-》b——a-》b c-》b——c-》a——b-》a——c-》b——a-》c——a-》b——c-》b
这样看,还是没能看出什么新的规律。那么我们再把每一大步再拆成小步看:
N=3时
a-》b——a-》c——b-》c
等价于
a-》b a-》c
a-》c (n=3) 对比 a-》b(n=2)
b-》c c-》b
我们发现这里所有的c和b换了位置,
仔细想一想,这里可以理解为:借中转柱子b把1、2盘移动到c处(n=3)
借中转柱子c把1、2盘移动到b处(n=2)
c-》a
c-》b
a-》b
这里可以理解为:借中转柱子a把1、2盘移动到b处
N=4:
a-》c——a-》b——c-》b——a-》c——b-》a——b-》c——a-》c
等价于
a-》c——a-》b——c-》b
a-》c
b-》a——b-》c——a-》c
为什么这里我这样划分呢?因为一开始我们认为这是一个递归算法问题,孩总结了三个大步骤,既然是能递归解决,也就是说这个函数必须能自己调用自己,那么步数上就必须是一样的,既然上面那些都不可再分且是三步,那么我就往三步上去靠。
虽然知道要分成三步,但具体怎么分?
a-》c——a-》b——c-》b
等价于
a-》c
a-》b
c-》b
发现没有?这个是不是和n=2的情况是一样的!
b-》a——b-》c——a-》c
等价于
b-》a
b-》c
a-》c
这个好像没有一样的,但是你仔细观察,可以看出规律,即三步的形式是一样的,只是柱子变了!你看,把b换成a,a换成c,c换成b不就是n=2的情况了?
“b换成a,a换成c,c换成b”这个操作是怎么个换法?
我们知道:
借中转柱子c把1、2盘移动到b处(n=2)
那么我们可以认为那个操作是指:
借中转柱子b把1、2盘移动到a处(n=2),只是有一个a换成c没有对应的地方,那么“a换成c”这个操作究竟是什么意思?
先不急,这个似乎一下子看不出来,我们先确定后面的那些步骤是不是也和这里一样通过
“b换成a,a换成c,c换成b”这种操作就变得和n=2的情况一样,为什么要确定这个?因为这意味着上面所有的步骤都是这个操作形成的,只是对象不一样了,都是这三步,只不过每一步移动的对象不一样!
那么,来吧!自己动手试试,你会发现确实是这样的!
让我把前面所有的总结在一张图上:
这回就一目了然了,每个红框都是一个操作,实际上就两种,每种的操作对象不一样而已。
我们应该一开始就能看到这个特点才对的(也就是开头所说的,解决n-1移动到柱子的问题、解决n-2移动到柱子的问题......的这个递归解决思路),这里也能看出其实“a换成c”这个操作是用来指出要移动的盘子在哪个柱子上的。
好了,解决了所有执行的问题,接下来就是怎么具体的实现这个执行!
我们之前解释过:
a-》b
a-》c
b-》c
这一段流程的意思是:借中转b把盘从a移动到c。这句话我们可以用一个函数来表示:Hanoi(n,a,c,b),这里的n先不管它,知道它是递归减少1的就好了。
这时候我们就可以把上面的图全部换成这个函数来表示了,顺带的也把n也写上去:
我失败了。。。。我不知道n应该填什么值。
自己试着把hanoi函数写一写,发现是自己不会看自调用函数的原因!于是停下来好好梳理了一番:
然后我懂了怎么看一个函数自调用:
顺便吐槽一下博客园的排版。。。明明在编辑界面排的很对齐,提交后在阅读界面看却完全不对齐。。。
好了,又解决了一个拦路虎!接下来继续前面的讨论。
你应该注意到了,我刻意用一个大箭头把各个移动步骤按顺序列了出来,由此我发现如果不是我写出这个函数来带入,根本不可能把n正确的写出来!
因为很明显的看到,a-》b是函数Hanoi(1,a,b,c)的结果,a-》c是Hanoi(2,a,c,b)的结果,而我原来以为a-》b——a-》c——b-》c这整个过程都是一个函数的结果!
其实a-》b——a-》c——b-》c 某种意义上确实是一个函数的结果,即 Hanoi(2,a,c,b)。但是不是我一开始以为的那样,因为Hanoi(2,a,c,b)这个函数本身又调用了两次自己才输出了a-》b和b-》c,只有a-》c才与它有直接关联,这一点必须要搞清楚。
参照上面的图可以完成n=3部分,但是n=4部分的n值如何填入还是不懂,怎么办?
会不会与n无关?先试试看:
我们来验证这个函数:
我用箭头把函数按时间线写了出来,至此我们算是解决这个问题了:
我们发现,红色、a-》b、绿色加起来就是n=4的情况,只保留黑色则是n=3的情况。
这意味着,这个H函数内部的两个函数各自只调用自己一次时,结果是n=4,不调用时结果是n=3.
从这里又能理解一点函数的一个内容,即“产出”:当一个函数A内还有一个函数B时,B函数可以调用,则输出的是B函数的结果,如果B函数不能调用输出的才是A函数的结果。(“产出”这个词是自己命名的,我不懂专业术语是什么.......我加双引号的都是这样来的;这段话可以参照上面照片里时间线函数中的1~4)
上面的分析可以让我们知道:
n=4,意味着H(a,b,c)函数只会自调用2次,n=3则只需要调用H(a,b,c)函数里的两个函数就够了。
但是这个伪代码是不完全的,因为我们发现n=1、2时不能正确输出!
所以我门要补全,从n=1开始。
我们知道n=1只输出一个a-》b,要想实现这一点必须加上一个分支——不能想着直接输出黑色的“cout a-》b”,因为要想输出这一段就必须执行上面的函数“H(A,C,B)”,所以必须加一个分支,这个分支只在n=1的时候输出,再加上n=3时只执行黑色,n=4时执行红色绿色,我们有:
这样对么?不不不,不对!既然函数加了一个else,里面的函数自调用怎么可能不加上这一步!所以应该是这样:
现在我们要把n拿回来考虑了,既然n=1才能执行else,那么if的条件就应该是n>1。
而n=3只能执行到黑色,n=4却能执行到红色绿色,意味着n的作用是觉得执行的“层数”,4比3多了1,那么很可能是递减照成的。
再想想n=2只输出H(a,b,c),而这个函数明显就是一开始输入的那个函数.....好像有点乱?没事,把这些情况和输出的语句列出来就好理解了:
看到这,就知道n的作用是觉得函数自调用的次数了,仔细想一想逻辑,就能知道最后构造的函数是下面这样:
写到这,这个问题已经解决了。最后总结一下。
回顾整篇文章,你会发现这是不断抽象后的结果:
1.以n=1、2、3、4为例,实际的把步骤以图画的模型画出来
2.把这些步骤用流程图的形式再抽象成“a-》c”
3.分析,流程图中有什么规律,需不需要先简化模型(即先把n无视的那一步)再继续讨论
4.找到规律。(递归的起点很重要,比如这里的n=1;递归的基础很重要,比如这里的一次只能移动一个盘等等)
5.把这套基本规律写成可执行的函数。
由此,我可以这么说:一旦发现某个问题可以分解成无数的小问题,而每个小问题都在进行类似的操作时,就可以尝试递归解决;具体的递归执行程序要靠不断抽象化,最终找到最基本的那些操作,把这些操作写成函数自我调用即可解决这个问题。
我在写图示时发现留白是有用的,随着思考的深入,必然要补充很多东西进去,不留白就必须重新画——这和函数的构造有异曲同工之妙,之前经常听别人说写的代码要能容易进行重构或者修补,这回算是有点理解了,只不过程序里补充东西可不是纸上的留白那么简单,具体是什么,目前还不能体会太多。
而且很多时候必要的重构是必须的,留白太多其实没用——很可能一开始就有什么想错了,必须重新写。这个度要怎么把握,就不得而知了。
最后贴个另一种表达流程的方式,也是从视频里看到的方法: