巧若拙(欢迎转载,但请注明出处:http://blog.csdn.net/qiaoruozhuo)
汉诺塔问题介绍:
在印度,有这么一个古老的传说:在世界中心贝拿勒斯(在印度北部)的圣庙里,一块黄铜板上插着三根宝石针。印度教的主神梵天在创造世界的时候,在其中一根针上从下到上地穿好了由大到小的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.csdn.net/qiaoruozhuo/article/details/40586443),我们可以发现,完全可以采用类似的方法把汉诺塔非递归算法转化为非递归算法,思维方式几乎一模一样。代码如下:
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); } } }
汉诺塔问题博大精深,我稍微搜集整理了一下,就得到如此多方法,还有好些方法一时不能理解,没有贴出来,请广大网友共同探讨,分享更多更好的方法。