• (2)汉诺塔问题与递归初探


    这篇文章起因是看网易云课堂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-》

    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.把这套基本规律写成可执行的函数。

    由此,我可以这么说:一旦发现某个问题可以分解成无数的小问题,而每个小问题都在进行类似的操作时,就可以尝试递归解决;具体的递归执行程序要靠不断抽象化,最终找到最基本的那些操作,把这些操作写成函数自我调用即可解决这个问题。

    我在写图示时发现留白是有用的,随着思考的深入,必然要补充很多东西进去,不留白就必须重新画——这和函数的构造有异曲同工之妙,之前经常听别人说写的代码要能容易进行重构或者修补,这回算是有点理解了,只不过程序里补充东西可不是纸上的留白那么简单,具体是什么,目前还不能体会太多。

    而且很多时候必要的重构是必须的,留白太多其实没用——很可能一开始就有什么想错了,必须重新写。这个度要怎么把握,就不得而知了。

    最后贴个另一种表达流程的方式,也是从视频里看到的方法:

    wuduojia
  • 相关阅读:
    【全网最优方法】JAVA初学:错误: 找不到或无法加载主类HelloWorld
    Anaconda官网下载太慢/出错,以及Anaconda下载包又慢又出错的总体方法,应该如何快速下载,使用上海科技大学的开源镜像站即可
    认识git的简单命令
    CSS、Bulma介绍
    IDEA问题java: -source 1.6 中不支持diamond、 lambda 表达式
    认识HTML语言(CodePen)
    【Java笔试】对数据库中的分解是否为无损连接和是否保持函数依赖的判定-由牛客网试题引申-保姆式教学
    【大唐杯经验2019】大唐杯全国移动通信大赛 国赛二等奖经验分享
    【Java笔试】OYO校招Java工程师|牛客平台,算法:字符串翻转。附选择题解析
    【Java面试宝典】正则表达式
  • 原文地址:https://www.cnblogs.com/wuduojia/p/7609065.html
Copyright © 2020-2023  润新知