汉诺塔问题非递归算法集锦

巧若拙(欢迎转载,但请注明出处: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);
		}
	}
}

汉诺塔问题博大精深,我稍微搜集整理了一下,就得到如此多方法,还有好些方法一时不能理解,没有贴出来,请广大网友共同探讨,分享更多更好的方法。

时间: 2024-10-07 06:54:29

汉诺塔问题非递归算法集锦的相关文章

汉诺塔的非递归算法

思路 模拟递归程序执行过程,借助一个堆栈,把递归转成非递归算法. 转化过程 1. 递归算法 1 void hanoi(int n, char from, char pass, char to) { 2 if (n == 0) 3 return; 4 5 hanoi(n - 1, from, to, pass); 6 move(n, from, to); 7 hanoi(n - 1, pass, from, to); 8 } 2. 处理首递归 本函数第2行是结束条件,第5行开始进入首递归.执行第5

5-17 汉诺塔的非递归实现 (25分)

5-17 汉诺塔的非递归实现   (25分) 借助堆栈以非递归(循环)方式求解汉诺塔的问题(n, a, b, c),即将N个盘子从起始柱(标记为"a")通过借助柱(标记为"b")移动到目标柱(标记为"c"),并保证每个移动符合汉诺塔问题的要求. 输入格式: 输入为一个正整数N,即起始柱上的盘数. 输出格式: 每个操作(移动)占一行,按柱1 -> 柱2的格式输出. 输入样例: 3 输出样例: a -> c a -> b c -&g

7-17 汉诺塔的非递归实现

7-17 汉诺塔的非递归实现(25 分) 借助堆栈以非递归(循环)方式求解汉诺塔的问题(n, a, b, c),即将N个盘子从起始柱(标记为"a")通过借助柱(标记为"b")移动到目标柱(标记为"c"),并保证每个移动符合汉诺塔问题的要求. 输入格式: 输入为一个正整数N,即起始柱上的盘数. 输出格式: 每个操作(移动)占一行,按柱1 -> 柱2的格式输出. 输入样例: 3 输出样例: a -> c a -> b c ->

汉诺塔的非递归实现(栈)

汉诺塔的非递归实现(栈) 美国学者找的规律:若是偶数,将a.b.c顺时针排列,否则a.c.b排列,然后反复做: (1)最小盘顺时针移动一个 (2)那两个柱子将最小的移动了,空的话直接移 借助堆栈以非递归(循环)方式求解汉诺塔的问题(n, a, b, c),即将N个盘子从起始柱(标记为"a")通过借助柱(标记为"b")移动到目标柱(标记为"c"),并保证每个移动符合汉诺塔问题的要求. 输入格式: 输入为一个正整数N,即起始柱上的盘数. 输出格式:

汉诺塔的图解递归算法

一.起源: 汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具.大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘.大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上.并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘. 二.抽象为数学问题: 如下图所示,从左到右有A.B.C三根柱子,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上

7-17 汉诺塔的非递归实现 (25分)

一开始看见通过了0.4+,以为是送分题,结果我错了. 花了好长时间看博客没搞懂怎么非递归实现(菜……). 后面看了 https://blog.csdn.net/computerme/article/details/18080511的算法和https://zhuanlan.zhihu.com/p/36085324的非递归实现方法受到启发才把代码给敲出来. 下面简单说一下我理解到的方法吧! 第一步是判断输入的n是奇数还是偶数,若为奇数,则按顺时针以ACB的顺序摆成品字型,若为偶数,则按顺时针以ABC

算法学习(4)----汉诺塔递归算法和非递归算法

学习<算法设计与分析基础>,习题2.4 第5题要求为汉诺塔游戏设计一个非递归的算法. 思,不得其解.看书后答案提示: 你如果做不到,也不要沮丧:这个问题的非递归算法虽然不复杂,但却不容易发现.作为一种安慰,可以在因特网上寻找答案. 好吧,话都说得这么直接了,遂百度之,得到一个感觉很好的答案,略做修改,摘录于下: 原文地址:http://blog.sina.com.cn/s/blog_48e3f9cd01000474.html ##################################

【Python学习】Python解决汉诺塔问题

参考文章:http://www.cnblogs.com/dmego/p/5965835.html 一句话:学程序不是目的,理解就好:写代码也不是必然,省事最好:拿也好,查也好,解决问题就好! 信息时代不用信息就是罪过,直接抄不加理解与应用,就不是自己的,下次遇到还是不会,或许其中的某一个细节就能够用于各个问题的解决,共勉 学习一个东西总会遇到一些经典的问题,学习Python第二天尝试看一下汉诺塔问题,还是百度,看看解题思路,纯粹是重温初中课堂,越活越回去了 汉诺塔的图解递归算法 一.起源: 汉诺

汉诺塔问题(用栈替代递归)

汉诺塔问题 古代有一个梵塔,塔内有三个座A.B.C,A座上有64个盘子,盘子大小不等,大的在下,小的在上(如图).有一个和尚想把这64个盘子从A座移到B座,但每次只能允许移动一个盘子,并且在移动过程中,3个座上的盘子始终保持大盘在下,小盘在上.在移动过程中可以利用B座,要求输出移动的步骤. 汉诺塔问题递归解法 C++代码 1 //By LYLtim 2 //2015.2.8 3 4 #include <iostream> 5 6 using namespace std; 7 8 void ha