汉诺塔(Tower of Hanoi),又称河内塔,是一个源于印度古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
要想把A柱上的四个圆片按相同的顺序放在C柱上,必须要借助B柱,并且一次只能拿一个圆片,因此在移动的过程中会出现两种情况,也就是从下往上看会有由大到小和由小到大的情况。
第一步:要把A柱上除了最大的圆片之外的三个放到B柱上,然后把A柱上的最大的圆片放到C柱上。
第二步:完成第一步之后,A柱就空出来了,再借助A柱,将B柱上的除了最下面的圆片之外的两个放到A柱上,将B柱的最下面的圆片放到C柱上。
第三步:完成第二步之后就把B柱空出来了,借助B柱,将A柱除了最下面的圆片的另一个圆片放在B柱上,将A柱的最下面的圆片放在C柱上。
第四步:将B柱上的最后一个圆片放在A柱上。
这样就完成了将A柱上的圆片按相同的顺序放在C柱上。
要想将汉诺塔的问题求解,就要从其中找到规律,找到解出问题的思路。
我们可以把三个柱子想象成三个数组,分别是arr1、arr2、arr3
整个过程中,只借助A柱和B柱,来进行移动,不涉及C柱,C柱只是依次放圆片。
按照上面的步骤,如下图
第一次:
对于arr1和arr2:
arr1[3] ===> arr2[0]
arr1[2] ===> arr2[1]
arr1[1] ===> arr2[2]
对于arr3:
arr1[0] ===> arr3[0]
第二次:
对于arr1和arr2:
arr2[1] ===> arr1[0]
arr2[0] ===> arr1[1]
对于arr3:
arr2[2] ===> arr3[1]
第三次:
对于arr1和arr2:
arr1[1] ===> arr2[0]
对于arr3:
arr1[0] ===> arr3[2]
arr2[0] ===> arr3[3]
最后一次只剩下了一个圆片,直接放在C柱即arr3上即可。
1.在移动中只借助arr1和arr2,因此移动得部分只需要设计arr1和arr2数组即可,并且每移动一次,arr1和arr2中就会有一个变成空的。
2.C柱也就是arr3放置的位置就会加一。
3.移动的过程中,会出现两种不同的情况,因此需要在移动的函数中设计不同的放置方法。
根据上面的思路和总结的规律,由于每一次移动的流程几乎是相同的,我觉得可以用递归来实现汉诺塔的流程,并且递归中最重要的两个条件:
1.限制条件是能够移动的圆片的数量,当圆片为0时表示移动完成
2.每调用一次函数,圆片数量减少一个,以此来不断逼近限制条件。
代码实现如下:
#include "hanoi.h"
int main()
{
int i = 0;
int arr1[SIZE] = {
4, 3, 2, 1 };
int arr2[SIZE] = {
0 };
int arr3[SIZE] = {
0 };
Hanoi(arr1, arr2, arr3,SIZE);
print(arr1);
print(arr2);
print(arr3);
return 0;
}
我在主函数中定义了3个数组,arr1代表A柱,arr2代表B柱,arr3代表C柱
arr1中,用4代表最大的圆片,1代表最小的圆片,也就是从下到上依次减小。
SIZE定义的是圆片的数量。
Hanoi函数是用来实现整个汉诺塔流程的。
void Hanoi(int* arr1, int* arr2, int* arr3, int sum)
{
if (sum > 1)
{
if (*arr2 == 0)
{
move2(arr1, arr2, arr3, sum);
clear(arr1);
Hanoi(arr1, arr2, arr3 + 1, sum - 1);
}
else if (*arr1 == 0)
{
move1(arr1, arr2, arr3, sum);
clear(arr2);
Hanoi(arr1, arr2, arr3 + 1, sum - 1);
}
}
else
{
if (*arr1 != 0)
{
*arr3 = *arr1;
clear(arr1);
}
else if (*arr2 != 0)
{
*arr3 = *arr2;
clear(arr2);
}
}
}
限制条件是可移动的圆片数量,在Hanoi函数中用sum表示。
这里的if条件设为大于1是因为,当圆片数量只剩一个的时候,可以直接放到arr3中,属于特殊情况,而数量大于1时,在不断进行递归。
判断为真之后,要判断哪一个数组为空,来借助这个数组进行移动,这也是为什么在主函数中将空数组初始化为0的原因,就是为了在这里方便判断。因为传参数过去传的是数组的首元素地址,但是因为整个数组都初始化为0,因此判断首元素是否为0就可以知道哪个数组是空的。
注:每次递归的函数有两个地方是变化的:
1.arr3的位置每次加一,因为每一次移动之后,arr3都会放置一个,所以要将其位置每次调用时进行更新。
2.sum的值每次减一,因为每次移动之后,可移动圆片的数量都会减少一个,不断地接近限制条件。
void move2(int* arr1, int* arr2, int* arr3, int sum)
{
int i = 0;
if (*arr1 > *(arr1 + 1))
{
for (i = sum - 1; i >= 1; i--)
{
*(arr2 + sum - i - 1) = *(arr1 + i);
}
*arr3 = *arr1;
}
else if (*arr1 < *(arr1 + 1))
{
for (i = sum - 1; i >= 1; i--)
{
*(arr2 + sum - i - 1) = *(arr1 + i-1);
}
*arr3 = *(arr1 + sum - 1);
}
}
void move1(int* arr1, int* arr2, int* arr3, int sum)
{
int i = 0;
if (*arr2 > *(arr2 + 1))
{
for (i = sum - 1; i >= 1; i--)
{
*(arr1 + sum - i - 1) = *(arr2 + i);
}
*arr3 = *arr2;
}
else if (*arr2 < *(arr2 + 1))
{
for (i = sum - 1; i >= 1; i--)
{
*(arr1 + sum - i - 1) = *(arr2 + i-1);
}
*arr3 = *(arr2 + sum - 1);
}
}
move1函数是当arr1数组为空时,借助arr1移动。
move2函数时当arr2数组为空时,借助arr2移动。
根据上面总结的规律,每次移动之前判断圆片的放置情况,从小到大是一种规律,从大到小是另一种规律;在移动之前先进行判断。
void clear(int* arr)
{
int i = 0;
for (i = 0; i < SIZE; i++)
{
*(arr + i) = 0;
}
}
这部分放在move1和move2函数之后,这个函数的目的是为了将移动之后的空的数组进行清零,方便下一次进行判断。
我在总结规律中发现,如果开始移动的时候是借助arr1数组,那么移动完之后arr2数组就是空的;如果开始移动的时候是借助arr2数组,那么移动完之后arr1数组就是空的,根据这个规律,我在move1函数后面将arr2清零;在move2函数后面将arr1清零,这样在下次判断时就会很方便。
void print(int* arr)
{
int i = 0;
for (i = 0; i < SIZE; i++)
{
printf("%d ", *(arr + i));
}
printf("\n");
}
这个函数是为了在最后打印三个数组来看看是否完成整个问题的求解。
根据这次完成汉诺塔问题的代码,使我对递归这部分有了更深刻的认识,递归在写的时候一定要注意两个必要条件,并且能用递归解决的问题一定是有规律可循的,如果递归越写越麻烦,肯定是不对的。写代码更多的是思想,先要在脑子里构思,再动手去写,这次的程序实现让我有了很大的收获。