| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 544 人关注过本帖, 1 人收藏
标题:汉诺塔问题非递归算法集锦
只看楼主 加入收藏
巧若拙
Rank: 4
来 自:宁波余姚
等 级:业余侠客
威 望:1
帖 子:159
专家分:273
注 册:2014-8-24
结帖率:46.15%
收藏(1)
已结贴  问题点数:20 回复次数:2 
汉诺塔问题非递归算法集锦
汉诺塔问题非递归算法集锦
巧若拙(欢迎转载,但请注明出处:http://blog.

汉诺塔问题介绍:
在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的64片金片,这就是所谓的汉诺塔。不论白天黑夜,总有一个僧侣在按照下面的法则移动这些金片,一次只移动一片,不管在哪根针上,小片必在大片上面。当所有的金片都从梵天穿好的那根针上移到另外一概针上时,世界就将在一声霹雳中消灭,梵塔、庙宇和众生都将同归于尽。
汉诺塔问题递归算法思路简单且代码简洁,算得上最美代码之一:
int count = 0;//全局变量,累积操作步数
void Hanoi(int n, char a, char b, char c)//递归算法
{
    if (n == 1)//如果只有一个盘,直接将其从a柱移动到c柱
        printf("%d: %d %c -> %c   ", ++count, n, a, c);
    else
    {
        Hanoi(n-1, a, c, b); //利用c柱中转,将n-1个盘从a柱移动到b柱
        printf("%d: %d %c -> %c   ", ++count, n, a, c);//将第n个盘从a柱移动到c柱
        Hanoi(n-1, b, a, c);//利用a柱中转,将n-1个盘从b柱移动到c柱
    }
}

    但是,非递归算法就没那么容易理解了,笔者搜集整理了以下几种方案(还有几种方案,由于本人一时未能理解,暂时不予贴出,同时请聪明的你补充简明易懂的算法,谢谢!)
    常见的思路是利用栈对递归算法进行非递归转换,李春葆老师编著的《数据结构(c语言篇)——习题与解析》(清华大学出版社)一书中介绍了一种方法。该方法设计了一个数据结构    struct act {int flag, num;  char x, y, z;} S[2000];  存储当前操作信息,其中flag==0表示直接移动num号圆盘,否则需要进一步分解;num表示当前操作移动盘子的编号,x,y,z表示当前操作对应的3个塔柱,分别是出发柱,中点柱和目标柱。
    算法的基本思路与中序遍历二叉树很类似,深入理解该算法后,我对书中的代码进行了一些改进,使其更简洁明了。代码如下:
void Hanoi_1(int n, char a, char b, char c)//非递归算法1
{
    struct act {int flag, num;  char x, y, z;} S[2000]; //存储当前操作信息,flag==0表示直接移动num号圆盘,否则需要进一步分解
    int top,  m;
    char ta, tb, tc;
   
    S[0].flag = 1;//初值入栈
    S[0].num = n;
    S[0].x = a;
    S[0].y = b;
    S[0].z = c;
    top = 0;
    count = 0;
    while (top >= 0)
    {
        if (S[top].flag == 0 || S[top].num == 1)//直接将num号圆盘从x移动到z
        {
            printf("%d: %d %c -> %c   ", ++count, S[top].num, S[top].x, S[top].z);
            --top;
        }
        else
        {  //提取栈顶信息
            m = S[top].num;
            ta = S[top].x;
            tb = S[top].y;
            tc = S[top].z;   
            //将 Hanoi(n-1, b, a, c); 操作入栈 ,覆盖原栈顶信息
            S[top].num = m - 1;
            S[top].x = tb;
            S[top].y = ta;
            S[top++].z = tc;   
            //将 将第m个盘从a柱移动到c柱 操作入栈
            S[top].flag = 0;
            S[top].num = m;
            S[top].x = ta;
            S[top++].z = tc;
            //将 Hanoi(n-1, a, c, b); 操作入栈
            S[top].flag = 1;
            S[top].num = m - 1;
            S[top].x = ta;
            S[top].y = tc;
            S[top].z = tb;
        }
    }
}

    非递归算法1是对递归算法的模拟过程,但由于其把当前操作分成两种类型,即直接移动和进一步分解,人为地增加了算法的复杂度。实际上,仔细分析二叉树的中序遍历非递归算法(详见拙作《史上最简明易懂非递归遍历二叉树算法》http://blog.),我们可以发现,完全可以采用类似的方法把汉诺塔非递归算法转化为非递归算法,思维方式几乎一模一样。代码如下:
void Hanoi_2(int n, char a, char b, char c)//非递归算法2
{
    struct act {int num;  char x, y, z;} S[MAX]; //存储当前操作信息
    int top = -1;
    int count = 0;
   
    while (n > 0 || top >= 0)
    {
        if (n > 0)//入栈,并搜索左孩子,即将 Hanoi(n-1, a, c, b); 操作入栈
        {  
            S[++top].num = n--;
            S[top].x = a;
            S[top].y = b;
            S[top].z = c;
            a = S[top].x;
            b = S[top].z;
            c = S[top].y;   
        }
        else//输出并退栈,搜索右孩子,即将 Hanoi(n-1, b, a, c); 操作入栈
        {
            printf("%d: %d %c -> %c   ", ++count, S[top].num, S[top].x, S[top].z);
            n = S[top].num - 1;
            a = S[top].y;
            b = S[top].x;
            c = S[top--].z;  
        }
    }
}

非递归算法1和2是利用栈模拟递归过程的基本方法,我们还可以从另外一个角度来分析汉诺塔问题。对于有n个盘子的汉诺塔问题,需要操作的步骤为2^n – 1,如果每一个步骤看成一个节点,则刚好构成一棵满二叉树,树高h与盘子数量的关系为h==n。结点所在的层数与对应盘子的编号关系为level==n+1-level,即盘子1在第n层,盘子n在第1层;若某个结点的操作为“盘子n从A->C”,则其左孩子操作为“盘子n-1从A->B”,右孩子操作为“盘子n-1从B->C”;中序遍历满二叉树,结点的编号恰好对应移动的次序。
因此我们可以构造一棵满二叉树,然后中序遍历该二叉树即可。代码如下:
void Hanoi_3(int n, char a, char b, char c)//非递归算法3,缺点是需要辅助空间太大
{
    struct act {int num;  char x, y, z;} BT[132000]; //存储每一步操作的满二叉树,假设n<=17.
    int S[MAX] = {0};
    int i, top, count = 0;
   
    BT[1].num = n;//为根结点赋值
    BT[1].x = a;
    BT[1].y = b;
    BT[1].z = c;
    n = pow(2, n-1);  
    for (i=1; i<n; i++)//为每个节点的左右孩子赋值
    {
        BT[i+i].num = BT[i+i+1].num = BT[i].num - 1;
        BT[i+i].x = BT[i+i+1].y = BT[i].x;
        BT[i+i].z = BT[i+i+1].x = BT[i].y;
        BT[i+i].y = BT[i+i+1].z = BT[i].z;
    }

    //中序遍历满二叉树
    n += n;
    i = 1;
    top = -1;
    while (i < n || top >= 0)
    {
        if (i < n)//入栈,并搜索左孩子
        {  
            S[++top] = i;
            i += i;
        }
        else//输出并退栈,搜索右孩子
        {
            i = S[top--];
            printf("%d: %d %c -> %c   ", ++count, BT[i].num, BT[i].x, BT[i].z);
            i += i + 1;  
        }
    }   
}

算法3的思路简明易懂,代码也很简洁,但是有一个致命缺陷,就是需要的辅助空间太多,当n较大时不太适用。有没有更好的方法呢?
    其实已经走到这一步了,再仔细观察一下这棵满二叉树,可以发现更多的规律(请读者自行画出满二叉树图,以便于理解算法)。以下是我的观察结果:
①树高h与盘子数量的关系为h==n。结点所在的层数与对应盘子的编号关系为level==n+1-level,即盘子1在第n层,盘子n在第1层;
②中序遍历满二叉树,结点的编号恰好对应移动的次序。
③第n层共2^(n-1)个结点,它们能被2^0整除且不能被2^1整除: 1,3,5,7,9,...2^n - 1.
  第n-1层共2^(n-2)个结点,它们能被2^1整除且不能被2^2整除: 2,6,10,14,...2^n - 2.
  ......
第3层 共2^2个结点,它们不能被2^(n-2)整除: 2^(n-3) * 1, 2^(n-3) * 2, 2^(n-3) * 3, 2^(n-3) * 4
  第2层 共2^1个结点,它们不能被2^(n-1)整除: 2^(n-2) * 1, 2^(n-2) * 2
  第1层 共2^0个结点,它不能被2^n整除: 2^(n-1) * 1
④奇数层各个结点的操作依次是:A->C、C->B、B->A、A->C、C->B、B->A、…模3循环;
  偶数层各个结点的操作依次是:A->B、B->C、C->A、A->B、B->C、C->A、…模3循环;
综合以上分析,得出以下结论
①盘子数N确定后,步骤总数m=2^n-1;
②第i(0<i<2^n)步所处的层数是确定的,当i能被2^(n-j)整除且不能被2^(n+1-j)整除时,i处在第j层;
③第i(0<i<2n)步在第j层的顺序号(从0开始)k=i/s,其中s = 2<<(n-j);
    根据上述分析,我们可以给出相应的代码:
void Hanoi_4(int n, char a, char b, char c)//非递归算法4,根据满二叉树的规律输出
{
    int i, level, k, s;
    int m = 1<<n;  
   
    for (i=1; i<m; i++)
    {
        for (s=2,level=n; i%s==0; s=s<<1)//判断是第几层的结点
        {
            --level;
        }
        
        k = i / s; //是第leve层第k+1个结点
        printf("%d: %d ", i, n+1-level);
        if (level & 1)//奇数层
        {
            switch (k % 3)
            {
                case 0: printf("A -> C   "); break;
                case 1: printf("C -> B   "); break;
                case 2: printf("B -> A   ");
            }
        }
        else  //偶数层
        {   
            switch (k % 3)
            {
                case 0: printf("A -> B   "); break;
                case 1: printf("B -> C   "); break;
                case 2: printf("C -> A   ");
            }
        }
    }
}

    算法3和4属于利用满二叉树特征而得出的方法,谈祥柏老师在《数学营养菜》中提到过一位美国学者发现的方法,只要轮流进行两步操作就可以了。
首先把三根柱子按顺序排成品字型,把所有的圆盘按从大到小的顺序放在柱子A上。根据圆盘的数量确定柱子的排放顺序:若n为偶数,按顺时针方向依次摆放 A B C;若n为奇数,按顺时针方向依次摆放 A C B。
(1)按顺时针方向把圆盘1从现在的柱子移动到下一根柱子,即当n为偶数时,若圆盘1在柱子A,则把它移动到B;若圆盘1在柱子B,则把它移动到C;若圆盘1在柱子C,则把它移动到A。
(2)接着,把另外两根柱子上可以移动的圆盘移动到新的柱子上。即把非空柱子上的圆盘移动到空柱子上,当两根柱子都非空时,移动较小的圆盘。这一步没有明确规定移动哪个圆盘,你可能以为会有多种可能性,其实不然,可实施的行动是唯一的。
(3)反复进行(1)(2)操作,最后就能按规定完成汉诺塔的移动。
有了这个步骤说明,代码是很容易写出来的。代码如下:
void Hanoi_5(int n, char a, char b, char c)//非递归算法5
{
    struct pillars {int S[MAX], top;  char pos;} P[3]; //存储各个柱子上的盘子信息   
    int i, count, next, pre;
   
    for (i=0; i<n; i++)
    {
        P[0].S[i] = n - i;
    }
    P[0].pos = a;
    P[0].top = n - 2; //1号盘不入栈
    P[1].top = P[2].top = -1;
   
    if (n % 2 == 0)
    {
        P[1].pos = b;
        P[2].pos = c;
    }
    else
    {
        P[1].pos = c;
        P[2].pos = b;
    }
   
    n = pow(2, n) - 1;  
    count = i = 0;
    while (count < n)
    {
        printf("%d: 1 %c -> %c   ", ++count, P[i%3].pos, P[(i+1)%3].pos);//将1号盘从顺时针移动到下一个柱子
        if (count == n)
            break;
        ++i;
        next = (i+ 1) % 3;
        pre  = (i - 1) % 3;
        if (P[next].top < 0 || P[pre].top >= 0 && P[pre].S[P[pre].top] < P[next].S[P[next].top])
        {
            P[next].S[++P[next].top] = P[pre].S[P[pre].top--];//将前一根柱子上的盘子移动到下一根柱子上
            printf("%d: %d %c -> %c   ", ++count, P[next].S[P[next].top], P[pre].pos, P[next].pos);
        }
        else
        {
            P[pre].S[++P[pre].top] = P[next].S[P[next].top--];//将下一根柱子上的盘子移动到前一根柱子上
            printf("%d: %d %c -> %c   ", ++count, P[pre].S[P[pre].top], P[next].pos, P[pre].pos);
        }
    }
}

汉诺塔问题博大精深,我稍微搜集整理了一下,就得到如此多方法,还有好些方法一时不能理解,没有贴出来,请广大网友共同探讨,分享更多更好的方法。
搜索更多相关主题的帖子: 印度教 黄铜板 传说 大片 中心 
2014-11-30 16:17
诸葛欧阳
Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19Rank: 19
来 自:流年
等 级:贵宾
威 望:82
帖 子:2790
专家分:14619
注 册:2014-10-16
收藏
得分:10 
为什么要用非递归方法

一片落叶掉进了回忆的流年。
2014-11-30 18:31
yahwei
Rank: 7Rank: 7Rank: 7
来 自:湖~
等 级:黑侠
威 望:3
帖 子:145
专家分:644
注 册:2011-11-10
收藏
得分:10 
回复 2 楼 诸葛欧阳
就这个问题来说,递归算法优点并不是它的效率有多高,而是由于它的代码可读必非常好。

[ 本帖最后由 yahwei 于 2014-12-1 12:57 编辑 ]

[qq]949654600[/qq]
2014-12-01 12:48
快速回复:汉诺塔问题非递归算法集锦
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.017451 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved