汉诺塔的一个实现细节问题
在网络上关于汉诺塔的实现模板中,总是出现一个move的函数;典型实现如下:
class Solution {
public:
void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
int n = A.size();
move(n, A, B, C);
}
void move(int n, vector<int>& A, vector<int>& B, vector<int>& C){
if (n == 1){
C.push_back(A.back());
A.pop_back();
return;
}
move(n-1, A, C, B); // 将A上面n-1个通过C移到B
C.push_back(A.back()); // 将A最后一个移到C
A.pop_back(); // 这时,A空了
move(n-1, B, A, C); // 将B上面n-1个通过空的A移到C
}
};
如果考察汉诺塔递归函数的定义:即将N个盘子从A 移动到C,中间通过B 的帮助来实现;初始时A是N个盘子,B,C都是空的;转移完成后A,B是空的,C是N个盘子;
那么我们可不可以不通过move函数而直接通过hanota函数来调用自己来实现呢?即将代码写成如下的形式:
class Solution {
public:
void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
if(A.size()==1)
{
C.insert(C.begin(),A[0]);
A.clear();
return;
}
int temp=A[A.size()-1];
vector<int> a(A.begin(),A.end()-1);
A.assign(a.begin(),a.end());
hanota(A,C,B); //将前n-1个盘子先送到B上;
C.insert(C.begin(),temp); //将最后一个盘子送到C
A.clear(); //保证helper初始时是空的;
hanota(B,A,C); //将B 的n-1个盘子在送到C上;
}
};
这样的代码直观看上去没有什么问题,但是运行起来确实错误的;
原因如下:
①首先,我们必须承认一个事实,即上面代码段中,最原始的ABC三根柱子是在运行过程中可以改变位置的,即原始的A有可能在某次调用过程中做B或C的位置,原始的B也有可能在AC的位置;原始的C也有可能在AB的位置;
在清楚一点说,我们把原始的ABC,记作[A],[B],[C];而把三个位置的按其作用和功能描述为【source】【helper】【destination】;上面那句话的意思就是在某一次调用时[A][B][C]完全都有可能是【source】【helper】【destination】中的任何一个;
②那么上面的代码问题就凸显出来了;即原本hanota函数的作用是将本轮次【source】的n盘子完全移动到【destination】,之后本轮次的【source】就没有盘子了,故而有A.clear();但是实际上,在某一次调用时,只是针对该轮次的【source】的盘子完全移动到目标位置,而不是作为【source】的柱子上的所有的盘子都要移动,即该柱子上面不属于该轮次的盘子并不需要移动;而上面的代码就会造成反复在两个瓶子里面来回倒水的情况;故而程序无法停止。
具体说来,比如当[A]的最下面一个盘子已经移动到[C]时,之后[C]的这个盘子就永远不会再参与移动;即使[C]在之后的某一轮次中担当【source】的作用;而在上面的代码直接将承担【source】的作用的[C]的最后一个盘子也会参与移动,这不就永远也没完没了了嘛。。。这本质是因为指针的滥用而混淆了递归函数的某次调用和全局指针地址之间的关系;
在递归中要尤其注意全局指针地址的问题。即要明确递归函数中的全局指针地址是否真的是你要的那个含义。。。
如果比喻成在两个杯中倒水的问题,上面hanota算法的本意是先将本轮次【source】中的n-1份水倒到【helper】中,然后再将本轮次的最后一份水倒入【destination】中,本轮次的n份水倒走后,实际上充当【source】作用的杯子内可能还是有全局的水的;但是因为指针地址的使用,造成的结果是每次都将【source】中所有全局的水全部倒走了,导致有限的水在不同的杯中倒来倒去,永远也无法收敛,因此程序无法停止。