问题:
汉诺塔问题(Hanoi),又称河内塔问题,是一个经典的数学游戏和益智玩具。它起源于印度,传说中一个古老的庙里,有一个柱子,在柱子上,有三个针,最左边的针上套着 64 个圆盘,大小不等,大的在下,小的在上。有一个和尚想把这 64 个圆盘从最左边的针上移到最右边的针上,但必须遵循以下规则:
每次只能移动一个圆盘;
每个圆盘只能放到比它大的圆盘上面。
问该如何移动圆盘,才能既按规定完成移动,且在移动圆盘的过程中始终保持稳定状态?
递归解法思路
我们定义一个 move
函数来实现盘子移动的逻辑。函数有四个参数:n
表示移动的盘子数量,a
、b
和 c
分别表示三根柱子(起始柱、辅助柱和目标柱)。
当
n
等于 1 时,表示只剩下一个盘子需要移动,那么直接将该盘子从起始柱a
移动到目标柱c
上,并输出移动的步骤和总步数。否则,我们将问题拆分为三个步骤:
- 将
n-1
个盘子从起始柱a
通过辅助柱c
移动到目标柱b
上。- 将剩下的最后一个盘子从起始柱
a
直接移动到目标柱c
上。- 将之前移动到辅助柱
b
上的n-1
个盘子通过起始柱a
移动到目标柱c
上。
这样,我们通过递归调用 move
函数实现了将 n 个盘子从柱子 A 移动到柱子 C 上的过程。
move函数代码:
// 定义递归移动函数
void move(int n, int a, int b, int c)
{
if (n == 1)
{
cout << a << "->" << c << endl; // 输出移动路径
sum++; // 步数+1
}
else
{
move(n - 1, a, c, b); // 将 n-1 个圆盘从柱子a经过柱子c移动到柱子b上(递归调用)
move(1, a, b, c); // 将最下面的一个圆盘从柱子a直接移动到柱子c上(递归调用)
move(n - 1, b, a, c); // 将 n-1 个圆盘从柱子b经过柱子a移动到柱子c上(递归调用)
}
}
主程序:
#include<iostream>
using namespace std;
void move(int n, int a, int b, int c);// 定义移动函数,
//参数包括圆盘数量n以及三个柱子的编号a、b、c
int sum = 0; // 记录移动步数
int main()
{
while (1)
{
int n;
cout << "请输入要移动的个数: ";
cin >> n;
move(n, 1, 2, 3); // 调用移动函数
cout << "总步骤:" << sum << "步" << endl;
sum = 0; // 重置步数为0
}
return 0;
}
尾递归优化(参考其它博客):
修改后的move函数:
// 尾递归移动函数的实现
void move(int n, int a, int b, int c, int &sum)
{
if (n == 1)
{
cout << a << "->" << c << endl; // 将编号为a的柱子上的圆盘移动到编号为c的柱子上(输出移动路径)
sum++; // 步数+1
return; // 返回,结束递归调用
}
move(n - 1, a, c, b, sum); // 将 n-1 个圆盘从柱子a经过柱子c移动到柱子b上(尾递归调用)
cout << a << "->" << c << endl; // 将编号为a的柱子上的圆盘移动到编号为c的柱子上
//(直接输出移动路径)
sum++; // 步数+1
move(n - 1, b, a, c, sum); // 将 n-1 个圆盘从柱子b经过柱子a移动到柱子c上(尾递归调用)
}
注:其中move(1,a,b,c)由 cout << a << "->" << c << endl;替换
尾递归优化的原理:
是通过将递归调用放置在函数的最后一个操作,从而达到减少函数调用开销和避免栈溢出的目的。
在普通的递归中,每次递归调用都需要在内存中创建一个新的函数调用栈帧,用于保存函数的局部变量、参数以及返回地址等信息。递归的深度增加时,这些栈帧会不断累积,导致内存消耗过大,甚至发生栈溢出。
而尾递归优化则在递归的最后一步操作完成后,直接返回结果,而不再进行额外的递归调用。这样,当前函数调用的栈帧就可以被重用,不再需要保留,从而减少了内存的使用。同时,由于没有新的函数调用,也就不会进一步增加递归的深度,避免了栈溢出的风险。
在汉诺塔问题中,尾递归优化的实现思路是将移动操作放置在进行递归调用之后,确保在递归过程中每一步的移动路径都会被立即输出,并累加步数。这样,在递归回溯的过程中,不会出现过多的函数调用栈帧,从而达到优化的效果。
总结起来,尾递归优化的原理是通过将递归调用放置在函数的最后一个操作,减少了函数调用开销和递归深度,提高了程序的效率和性能。
时间复杂度:
假设总的移动步数为 sum,每次移动时 sum 自增 1。经过分析可得总的移动步数为 sum = 2 * sum(n-1) + 1。 递归深度为 n,总步数和n的关系为 T(n)=2^n-1,所以时间复杂度为O(2^n)。