汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
玩法:
1每次仅允许移动一个碟子的位置。
2在任意一次移动中,较小的盘子不得被置于较大的盘子下方。
3全部圆盘在起始柱上,最后所有圆盘要到目标柱上。
由此玩法易知,我们需要将最大的先移动到目标柱。
分析如下:
设汉诺塔层数为n,起始柱为A,过度柱为B,目标住为C:
n=1
A→C
移动次数 1
n=2
A→B(形成一个一层的汉诺塔), A→C (实现将大的移动到目标柱),B→C(将一层的汉诺塔移动)
移动次数 3=2+1
2(这两步形成一个最大的和一个一层的汉诺塔)+1(将一层汉诺塔移动的步数)
n=3
A→C A→B C→B(这三步在B上形成一个两层的汉诺塔,A→C(实现将最大的移动到目标柱), B→A B→C A→C (这三步将两层的汉诺塔移动)
移动次数 7=4+3
4(这四步形成一个最大的和一个两层汉诺塔)+3(将两层汉诺塔移动的步数)
n=4
A→B A→C B→C A→B C→A C→B A→B(这七步在B上形成一个3层汉诺塔),A→C (实现将最大的移动到目标柱)
B→C B→A C→A B→C A→B A→C B→C(将三层的汉诺塔移动)
移动次数 15=8+7
8(这八步形成一个最大的和一个三层汉诺塔)+7(将三层汉诺塔移动的步数)
…………
……
由上例可知,我们的核心思想是,将一个n层的汉诺塔分成一个(n-1)层的汉诺塔和一个最大层在目标柱上。
递归的定义:
一个函数在调用另一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。
不难发现,一个以A为起始柱的n层的汉诺塔,先将(n-1)的汉诺塔层移动到B,再将第n层移动到C,最后将(n-1)层的汉诺塔由B移动到C。
由此可以得出,n层汉诺塔移动的次数 等于2倍的(n-1)层的汉诺塔的移动次数 加 第n层的的移动次数,也就是1。
代码如下:
#include<stdio.h>
void main()
{
int hnt(int n);
int n,ret;
printf("请输入汉诺塔的层数n:\0");
scanf("%d",&n);
ret = hnt(n);
printf("%d",ret);
}
int hnt(int n)
{
if(n==1)
return 1;
if(n>1)
return 1+2*hnt(n-1);
}
但这种方法只能得到移动的次数,而无法得到移动的路径。
同时,我们也很容易发现,若n层汉诺塔是以A为起始柱,(n-1)层汉诺塔在过渡柱B上形成,进行(n-2)层分离时,则相当于以B为起始柱,A为过渡柱。但C柱确是固定的。
逆向的分析一下,易知:(以4层和5层为例,具体逆推。)
为方便陈述,按从小到大,分别称为1号2号3号4号5号。
注:n层塔就是从小到大的标准汉诺塔。比如,两层塔指的是1号,2号这种标准的汉诺塔,而不是任意两个。位置指的是在分离出最大的那一个后,塔所在的位置,而不是在正向移动过程中,所形成的x层塔的位置。(此时,最大的那些层在C,一个柱子是标准汉诺塔,另一个柱子为空)
4层:
C柱上放4号,B柱上放一个三层的汉诺塔,所以3号在B柱。
B柱上放3号,A柱上放一个两层的汉诺塔,所以2号在A柱。
A柱上放2号,B柱上放一个一层的汉诺塔,所以1号在B柱。
B柱并不是起始柱,所以肯定是移动后落在的B柱。
B柱上放一号,由此我们便知道了第一个放在哪。
5层:
C柱上放5号,B柱上放一个四层的汉诺塔,所以4号在B柱。
B柱上放4号,A柱上放一个三层的汉诺塔,所以3号在A柱。
A柱上放3号,B柱上放一个两层的汉诺塔,所以2号在B柱。
B柱上放2号,A柱上放一个一层的汉诺塔,所以1号在A柱。
A柱是起始柱,所以第一次移动,需要将一号移动到C柱。
C柱上放一号,由此我们便知道了第一个放在哪。
当遇到一个n层的汉诺塔
n为偶数,则第一个放在B柱,二层塔在A柱形成,三层塔在B柱形成……(n-2)层塔在A柱形成,(n-1)层塔在B柱形成,n层在C柱形成。
n为奇数,则第一个放在C柱,二层塔在B柱形成,三层塔在A柱形成……(n-2)层塔在C柱形成,(n-1)层塔在B柱形成,n层在C柱形成。
因此,通过以上验证,我们可以得知第一次应该移动到哪个位置。
由以上分析,将其具体成以下几步。
1.将(n-1)层汉诺塔,从A经C移动到B.
2.将A杆上第n层移动到C.
3.将(n-1) 层汉诺塔 ,从B经A移动到C.
我们需要先定义一个函数move,用来实现移动途径的显示。
定义汉诺塔函数,首先需要传入塔的层数n,并且需要三个柱子的位置,从而实现将第一个位置通过第二个位置,传给第三个位置。
做后通过主函数调用hnt函数,得到结果。
代码如下:
#include <stdio.h>
void main()
{
int n;
void hnt(int n,char qs,char gd,char mb);
printf("请输入层数n:");
scanf("%d",&n);
hnt(n,'A','B','C');
}
void hnt(int n,char qs,char gd,char mb)
{
void move(char a,char b);
if(n==1)
move(qs,mb);
if(n>1)
{
hnt(n-1,qs,mb,gd);
move(qs,mb);
hnt(n-1,gd,qs,mb) ;
}
}
void move(char a,char b)
{
printf("%c→%c ",a,b);
}
汉诺塔函数内部递归分析
在网上找了张递归调用示意图,通过此图,来看一下汉诺塔函数是怎样调用的。
比如我们执行到第四个语句,那个就会在堆栈存储第四行,以及四个形参。
附:当然这只是一种便于我们理解的写法,在计算机中当然以二进制存储。
以一次汉诺塔函数递归为例。
第一个,得到传入n=3,x=a,y=b,z=c。n不等于1,再次执行汉诺塔函数。n-1=2,x=a,z=b,y=c。
第二个,得到传入n=2,x=a,y=c,z=b。n不等于1,再次执行汉诺塔函数。n-1=1,x=a,z=c,y=b。
第三个,得到传入n=1,x=a,y=b,z=c。n等于1,执行move函数,输出a→c。并返回到第二个汉诺塔函数,从堆栈中得到传入n=2,x=a,y=c,z=b。执行move函数,输出a→b。再执行第二个汉诺塔函数中的第二个汉诺塔函数。以下相似,不再文字解释,参照此图。
ps:
递归所能解决的问题通常都有与之相对应的非递归算法,但有时非递归算法非常复杂,所以对于本身具有很强递归特性的问题通常选择递归方法解决,尽管递归方法在运算过程中需要使用大量的栈空间(属于动态存储空间的一部分),且速度比非递归算法一般都要慢一点。
通俗来说,递归就是时间换空间。虽然所写代码量不多,但是运行时间一般会很长。