汉诺塔问题的递归程序很简单,也很容易理解,C源程序如下。
// 程序1
#include <stdio.h>
int n;
void move(int k, char from, char to) {
static int Ser_num;
Ser_num++;
printf("%3d k=%2d %c-->%c\n",Ser_num,k,from, to);
return;
}
void hanoi(int k,char from,char pass,char to) {
if(k==0) return;
else {
hanoi(k-1,from,to,pass);
move(n-k+1,from,to);
hanoi(k-1,pass,from,to);
}
}
int main() {
printf("请输入A座上的盘子数目(0<=exit):");
scanf("%d",&n);
if (n>0){
printf("序号 盘号 移动\n");
hanoi(n,'A','B','C');
}
return 0;
}
汉诺塔问题的非递归程序实现要复杂一些,在上一篇文章中介绍了一种借助于堆栈,解决该问题的解法。下面根据文献1描述的方法介绍一种有趣的汉诺塔问题非递归方法。
文献1利用满二叉树的性质提出了汉诺塔问题的非递归算法,为便于叙述,假设AC为满二叉树的一个结点,它表示金盘从A柱移到C柱,同理BC结点表示金盘从B柱移到C柱,其他类推。
那么,你发现汉诺塔与满二叉树有什么关系呢?下面简要介绍。
性质1:n个盘子的汉诺塔问题需要移动盘子的总数为2n-1,而深度为n的满二叉树正好有2n-1个结点,汉诺塔的每一个盘子的移动都对应满二叉树中的一个结点。很奇妙吧!
当n=1时,汉诺塔中的盘子仅需要移动1次,即1A->C(注:此处的数字指第几层的盘子,后面相同),对应于仅有一个根结点AC的满二叉树,如图1所示n=1的情况。
当n=2时,移动3次,分别是2A->B,1A->C,2B->C,对应满二叉树中的结点AB,AC,BC,如图1所示n=2的情况。
当n=3时,移动7次,如图1所示n=3的满二叉树。
图1 满二叉树与汉诺塔的关系
性质2:n=2的满二叉树是由n=1的满二叉树派生的,n=3的满二叉树是由n=2的满二叉树派生的,其他类推。例如,结点AB派生的左右子树分别是AC和CB。即设P={A,B,C},若结点为sd,其中s∈P,d∈P,那么,结点sd派生出的左右子树的根结点分别是sp和pd,其中p∈P-{s, d},例如AB派生出的两个子结点分别是AC和CB。
性质3:盘子的移动顺序是满二叉树的中序遍历。例如深度为3的满二叉树的中序遍历为:AC、AB、CB、AC、BA、BC、AC。正好对应3A->C,2A->B,3C->B,1A->C,3B->A,2B->C,1A->C。
除了上面3个性质外是否还有其他性质呢?我们继续寻找。
下面给出完成的源程序。
#include <stdio.h>
int n;
void move(int k, char from, char to) {
static int Ser_num;
Ser_num++;
printf("%3d k=%2d %c-->%c\n",Ser_num,k,from, to);
return;
}
void hanoi(int n) {
int movenum=1;
int i,m,n_k,k,j;
for (i=0; i<n; i++) {
movenum *= 2;
}
movenum--; // 计算移动盘次数
for (i=1; i<=movenum; i++) {
m=i;
n_k=0;
while (m%2==0) {
n_k++;
m=m/2;
}
k=n-n_k;
j=m/2;
if (k%2==0) {
switch(j%3) {
case 0: move(k,'A','B'); break;
case 1: move(k,'B','C'); break;
case 2: move(k,'C','A'); break;
}
} else {
switch(j%3) {
case 0: move(k,'A','C'); break;
case 1: move(k,'C','B'); break;
case 2: move(k,'B','A'); break;
}
}
}
}
int main() {
printf("请输入A座上的盘子数目(0<=exit):");
scanf("%d",&n);
if (n>0) {
printf("序号 盘号 移动\n");
hanoi(n);
}
return 0;
}
参考文献:
[1] 谭罗生,吴福英,黄明和. Hanoi塔问题的解模型[J]. 计算机应用与软件,2004,21(10):49-51.