Write a program to move the disks from the first rod to the last using Stacks
译文:就是利用栈这一数据结构来解决汉诺塔问题。
汉诺塔是一个经典问题:有三根杆子编号 A,B,C,A 上有N(N>1)穿孔圆盘,盘的尺寸由下到上依次变小,呈塔形,要求按以下规则将盘移动到C:
利用栈解决汉诺塔问题。
一、递归
对于汉诺塔这个经典问题,我们先用递归来实现(递归:不必纠结于内部实现,明确其功能即可)
如上图所示,要将 A 杆的 n 个圆盘移动到 C 盘,B 盘则是作为一个中介,假定初始状态为(1~n, 0, 0),那么其最终状态为(0, 0, 1~n)。考虑到递归实现,则必然会有这样一个中间状态(n,1~n-1,0),然后其下一个状态就是(0,1~n-1,n),编号 n 的最大盘位置已经正确,这样等效于(0,1~n-1,0)n位置已经确定可忽略,这个状态和初始状态如出一辙,此时 A 盘成了中介盘,然后就是(1~n-2,n-1,0),接下来便是(1~n-2,0,n-1)已经确定好位置的忽略,这样 n 和 n-1 位置确定,忽略,就变成了初始状态(1~n-2,0,0)。实际状态为(1~n-2,0,n-1~n)。当 A 只有一个盘子,也就是状态为(n,0,0)时(位置确定的忽略)直接移动到 C 盘,当 n = 1 时,为递归的终止条件。
将上面整理一下:移动过程中有三个状态
初始状态:(1~n,0,0)
中间状态:(n,1~n-1,0)
最终状态:(0,0,1~n)
先定义汉诺塔函数原型
void hanoi(int n, char A, char B, char C);函数的功能就是:A 借助 B 移动 n 个盘到 C。(其中 n A B C 均表示函数 hanoi 对应位置参数,而不是某个具体盘)
总体:很明显初始状态(1~n,0,0)达到最终状态(0,0,1~n)就是
hanoi(n, A, B, C); //A 借助 B 移动 n 个盘到 C分步一:先要从初始状态(1~n,0,0)进入中间状态(n,1~n-1,0)就是
hanoi(n-1, A, C, B); //A 借助 C 移动 n-1 个盘到 B分步二:然后从中间状态(n,1~n-1,0)达到最终状态(0,0,1~n),由于此时的中间状态 A 上只有一个盘子,且为未确定位置中的最大盘子,则直接将该盘子移动到 C,然后进入下一轮的递归。所以接下来进入递归的是这样一个情况:
从中间状态(0,1~n-1,0)达到最终状态(0,0,1~n)就是
hanoi(n-1, B, A, C); //B 借助 A 移动 n-1 个盘到 C每一轮后有一个盘子确定好位置,即下一轮只需考虑 n-1 个盘子。
下面贴程序:
这里修改一下函数原型,使其可以返回移动的次数。
using namespace std; void hanoi(int n, char A, char B, char C, int *cnt) { if (1 == n) { ++(*cnt); cout << "直接将编号为" << n << "的盘子从" << A << "移动到" << C << endl; } else { ++(*cnt); hanoi(n - 1, A, C, B, cnt); cout << "直接将编号为" << n << "的盘子从" << A << "移动到" << C << endl; hanoi(n - 1, B, A, C, cnt); } } int main() { int n; int cnt = 0; cout << "输入要移动的盘子个数 n = "; cin >> n; hanoi(n, 'A', 'B', 'C', &cnt); cout << "移动的总次数为:" << cnt << endl; return 0; }输入数值的时候,不要手一抖输入 64 或其余相对大的值,不然。。。
二、栈
这里就是真正的解答题目了,递归是一个天然栈,我们可以沿用前面递归的思想,将其通过栈来实现。
前面讨论到汉诺塔的移动要经过以下几个状态
初始状态:(1~n,0,0)
中间状态:(n,1~n-1,0)
次中间状态:(0,1~n-1,n)
最终状态:(0,0,1~n)
上面的第二个中间状态是当 A 只有一个盘子时,就直接将其移动到 C 盘。
这里是将移动过程状态压栈,首先需要定义一个数据结构来保存这些操作的过程状态。
struct oprt { int begin, end; char src, bri, dst; oprt(){} oprt(int _begin, int _end, char _src, char _bri, char _dst) :begin(_begin), end(_end), src(_src), bri(_bri), dst(_dst){} };上面的 begin 和 end 表明 src 参数位置盘子的起止位置,当(begin == end)表示 src 参数只有一个盘子,这里指的是第三个参数标明的杆的盘子情况。
后面三个参数位置表示:src 通过 bri 将盘子移动到 dst。(将src,bri,dst 换为参数指定的位置更容易理解)
前面说明有四个状态,那么就有三个过程状态,分别为:
初始状态 --> 中间状态:(1~n,0,0) --> (n,1~n-1,0)
中间状态 --> 次中间状态:(n,1~n-1,0) --> (0,1~n-1,n)
次中间状态 --> 最终状态:(0,1~n-1,n) --> (0,0,1~n)
其中,中间状态 --> 次中间状态,A 杆只有一个盘子,那么直接将该盘子移动到 C 盘即可(同前面递归一样)。
这些过程需要进行压栈出栈处理,压栈的时候不作处理,出栈时进行处理,所以在压栈顺序与实际顺序相反。一开始我们把目的压栈,即(初始状态-->最终状态)压栈,然后将其弹出,并判断其是否为原子操作,也就是说是否只需移动一个盘,如果是则直接移动,否则将其分解为三个状态,然后再行判断,直至操作为原子操作。
void hanoi(int n, char src, char bri, char dst, int *cnt) { stack<oprt> Stack; oprt temp; Stack.push(oprt(1, n, src, bri, dst)); //初始状态 ——> 最终状态 while (!Stack.empty()) { temp = Stack.top(); Stack.pop(); if (temp.begin != temp.end) //非原子操作,分解 { //次中间状态 ——> 最终状态 //将B中1~n-1的盘子通过A移动到C Stack.push(oprt(temp.begin, temp.end - 1, bri, src, dst)); //中间状态 ——> 次中间状态 //将A中编号n的盘子通过B移动到C Stack.push(oprt(temp.end, temp.end, src, bri, dst)); //初始状态 ——> 中间状态 //将A中1~n-1的盘子通过C移动到B Stack.push(oprt(temp.begin, temp.end - 1, src, dst, bri)); } else //原子操作,直接移动 { cout << "Move disk " << temp.begin << " from " << temp.src << " to " << temp.dst << endl; ++(*cnt); //移动次数 } } }原子操作也叫不可分割的操作,这里是说只移动一个盘子。上面程序需要理解的是压栈保存的是过程状态,不是单一的状态。