汉诺塔是一个经典的递归算法案例,下面来描述问题:
汉诺(Hanoi)塔问题:古代有一个梵塔,塔内有三个座A、B、C,A座上有64个盘子,盘子大小不等,大的在下,小的在上(如图)。有一个和尚想把这64个盘子从A座移到C座,但每次只能允许移动一个盘子,并且在移动过程中,3个座上的盘子始终保持大盘在下,小盘在上。在移动过程中可以利用B座,要求打印移动的步骤。
怎么解决这个问题呢,如果你已经很熟悉了,那就不用细究了,不过其实其分析过程也是很有代表性的(先从简单的开始,并向复杂扩展):
这个问题在盘子比较多的情况下,很难直接写出移动步骤。我们可以先分析盘子比较少的情况。假定盘子从大向小依次为:盘子1,盘子2,...,盘子64。
如果只有一个盘子,则不需要利用B座,直接将盘子从A移动到C。
如果有2个盘子,可以先将盘子1上的盘子2移动到B;将盘子1移动到c;将盘子2移动到c。这说明了:可以借助B将2个盘子从A移动到C,当然,也可以借助C将2个盘子从A移动到B。
如果有3个盘子,那么根据2个盘子的结论,可以借助c将盘子1上的两个盘子从A移动到B;将盘子1从A移动到C,A变成空座;借助A座,将B上的两个盘子移动到C。这说明:可以借助一个空座,将3个盘子从一个座移动到另一个。
如果有4个盘子,那么首先借助空座C,将盘子1上的三个盘子从A移动到B;将盘子1移动到C,A变成空座;借助空座A,将B座上的三个盘子移动到C。
得出的结论是,指定一个空座,指定一个源座,指定可以借助的座,可以将源座上所有的盘子都移动到空座上。而且小数量的盘子的移动方法可以被多数量的盘子的移动方法所利用。算法的表示是在移动n个盘子从A到C时,先将n-1个盘子从A移动到B,然后移动第n个盘子到C,然后再将n-1个盘子从B移动到C。可以很容易用递归实现,下面是算法的实现:
using namespace std;
void hanoi(int n, char src, char resource, char dest)
{
if(n>1)
{
hanoi(n-1, src, dest, resource);
cout<<"Move "<<n<<" from "<<src<<" to "<<dest<<endl;
hanoi(n-1, resource, src, dest);
}
else
{
cout<<"Move "<<n<<" from "<<src<<" to "<<dest<<endl;
}
}
int main(int argc, char **argv)
{
int N;
cout<<"Please Input a number:"<<endl;
cin>>N;
hanoi(N,'A', 'B', 'C');
}
应该说,汉诺塔的递归算法是非常简单的。 但是都说递归算法都可以变成迭代算法,那汉诺塔的迭代算法又如何实现呢?
先从理论上看看要设计好迭代算法,应该注意哪些方面:
一、确定迭代变量。在可以用迭代算法解决的问题中,至少存在一个直接或间接地不断由旧值递推出新值的变量,这个变量就是迭代变量。
二、建立迭代关系式。所谓迭代关系式,指如何从变量的前一个值推出其下一个值的公式(或关系)。迭代关系式的建立是解决迭代问题的关键,通常可以使用递推或倒推的方法来完成。
三、对迭代过程进行控制。在什么时候结束迭代过程?这是编写迭代程序必须考虑的问题。不能让迭代过程无休止地重复执行下去。迭代过程的控制通常可分为两种情况:一种是所需的迭代次数是个确定的值,可以计算出来;另一种是所需的迭代次数无法确定。对于前一种情况,可以构建一个固定次数的循环来实现对迭代过程的控制;对于后一种情况,需要进一步分析出用来结束迭代过程的条件。
通俗一点去理解呢,就是如果要设计迭代算法,一定要搞清楚迭代变量是什么,在什么范围内迭代,迭代时状态的迁移是怎样完成的。举个简单的例子,
for(int i= 0;I <1000;i++);
这是一个非常简单的迭代过程,迭代变量是i,范围是从0到1000的一个数列,状态迁移是+1。
其实很多时候迭代范围都可以理解称为一个数据结构,按某种顺序将这个数据结构从某个点遍历都另外一个点。上面的小例子可以理解为数组的遍历。所以设计迭代算法时一定要知道你在遍历一个什么样的数据结构.
下面我们看看汉诺塔的迭代算法如何实现:
1. 我们先来分析一下移动的过程:先将n-1个盘子从A移动到B,把第n个盘子从A移动到C,把n-1个盘子从B移动到C。这本是递归的过程,但我们要想知道迭代操作的最终的数据结构,就一定要将其展开为原子操作。下面用图来解释:
这个图看起来就会更容易明白了,移动盘子的三个步骤就跟一个数一样,第一步移动若干盘子是左子树;第二步移动一个盘子相当于根;第三步移动若干盘子又相当于右子树。而且每棵子树都可以按照同样的步骤分解,一直到全部是移动一个盘子的操作,至此,才算把整个数据结构展开了,我们也应该知道这是一个什么样的数据结构了:二叉树。这个二叉树的深度是N,也就是盘子的个数,根节点表示盘子N的操作,孩子是N-1时的移动,. . . . 。移动的过程就是先左子树,根节点,右子树,也就是前序遍历。然后我们再看每个节点的表达式是什么?画个图,看了一下,要找到每个节点的表达式是不容易的。不过,我们应该可以发现一个重要线索:只要最左面的节点知道了,就可以知道父节点和兄弟节点的表达式了。比如左孩子是m,A,B,那父节点是m+1,A,C, 右孩子是m,B,C。 所以关键就是要知道最左孩子的表达式。列出N = 1, 2,3 时的操作顺序可以发现,最左孩子只是在A,B和A,C之间交替。而且N为偶数时,为A,B;N为奇数时为A,C。至此我们对我们要迭代的数据结构就足够清楚了。下面就是实现一个满二叉树的前序遍历算法了。代码实现如下所示:
#include <stack>
using namespace std;
class TreeNode
{
public:
TreeNode(int N, char _src, char _dest){ plateNum = N;src =_src; dest = _dest;}
int plateNum;
char src;
char dest;
};
void hanoi(int N)
{
stack<TreeNode *> *_stack = new stack<TreeNode *>();
_stack->push(new TreeNode(N, 'A', 'C'));
TreeNode * current = _stack->top();
while(!_stack->empty())
{
while(current->plateNum > 0)
{
current = new TreeNode(current->plateNum-1,current->src,'A'+'B'+'C' - current->dest - current->src);
_stack->push(current);
}
current = _stack->top();
_stack->pop();
cout<<"Move "<<(current->plateNum+1)<<" from "<<current->src<<" to "<<current->dest<<endl;
if(current->plateNum != 0)
{
TreeNode* old = current;
current = new TreeNode(current->plateNum-1,'A'+'B'+'C' - current->dest - current->src, current->dest);
_stack->push(current);
delete old;
}
}
delete _stack;
}
int main(int argc, char **argv)
{
int N;
char src = 'A',resource='B',dest = 'C';
bool moved = false;
cout<<"Please input a number:"<<endl;
cin>>N;
hanoi(N-1);
}
另外一种实现: http://shb0629.blog.163.com/blog/static/42201810200932331159721/
还有一种实现:http://www.cppblog.com/goal00001111/archive/2006/06/07/8252.html