问题描述
问题分析
如果是初次接触类似的问题,乍看之下肯定会感觉无从下手。
要把64个圆盘从a柱子移动到c柱子上,第一步应该怎么做?虽然可以肯定,第一步唯一的选择是移动 a 最上面的那个圆盘,但是应该将其移到 b 还是 c 呢?很难确定。因为接下来的第二步、第三步……直到最后一步,看起来都是很难确定的。能立即确定的是最后一步:最后一步的盘子肯定也是 a 最上面那个圆盘,并且是由 a 或 b 移动到 c——此前已经将 63 个圆盘移动到了 c 上。
也许你会说,管他呢,先随便试着移动一下好了。如果你这么做,你会发现,接下来你会面临越来越多类似的选择,对每一个选择都“试”一下的话,你会偏离正确的道路越来越远,直到你发现你接下来无法进行为止。
如果将这个问题的盘子数量减为10个或更少,就不会有太大的问题了。但盘子数量为64的话,你一共需要移动约1800亿亿步(18,446,744,073,709,551,615),才能最终完成整个过程。这是一个天文数字,没有人能够在有生之年通过手动的方式来完成它。即使借助于计算机,假设计算机每秒能够移动100万步,那么约需要18万亿秒,即58万年。将计算机的速度再提高1000倍,即每秒10亿步,也需要584年才能够完成。
虽然64个盘子超出了人力和现代计算机的能力,但至少对于计算机来说,这不是一个无法完成的任务,因为与我们人类不同,计算机的能力在不断提高。
-
对于 64 个盘子的问题有点烧脑,所以我们先来看两个盘子的时候,整个过程很简单:
-
- 首先将第一个盘子挪到 B,A–>B
- 再将第二个盘子挪到 C,A–>C
- 将第一个盘子挪到 C,B–>C
-
然后我们再转过头来看 64 个盘子的问题;我们能够发现,如果我们把整个问题划分阶段:
- 将前 63 个盘子从 A --> B
- 将最后一个盘子从 A–>C
- 将 63 个盘子从 B–>C
- 将前 63 个盘子从 A --> B
有人可能说了,你只是笼统的这么类比,把前 63 个盘子当做 两个盘子中的第 1 个盘子处理;但是你总觉得不能这么简单的类比,因为这样总有种忽略中间细节、让人不踏实的感觉。没错,你的感觉是对的。初学递归本来就会给你一种不踏实的感觉,你总觉得他与直觉相悖,如果你仍觉得不太放心,我们继续再往下看:
要深入理解递归的原理,就要知道,递归的特点就是:解决的问题的范围越来越小,但是解决的方法在每一层都是一样的,对 2 个数的时候是这样的方法,对 64 个数也是一样的。那你又要说了:“都是一样的不就死循环了?”
是的,这让我想起了一个故事:“从前有座山,山里有座庙,庙里有个老和尚和一个小和尚,老和尚给小和尚讲故事:‘从前有座山,山里有座庙,庙里有个。。。。’” 这确实是死循环,所以他不是递归,真正的递归有出口,什么意思呢?? 意思就是,虽然递归的方法在每一次调用的时候都是一样的;他仍然存在唯一一次例外,那就是递归结束的时候。
在汉诺塔这个例子中,唯一一次不用 A–>B, A–>C, B–>C 的过程,就是只有一个盘子的时候。 这个时候,直接从 A–>C 就可以了。其他的时候,无论多少个盘子,都是从A–>B, A–>C, B–>C 这个过程。
接下来,我们开始关注细节,也就是递归中的具体实现过程,我们可以发现,虽然无论是两个盘子还是 64 个盘子,都是完成的 A–>B, A–>C, B–>C 过程;但是显然在 64 个盘子转移的过程中,A–>B 和 B–>C 这两个过程都涉及到 63 个盘子的转移,而只有A–>C 过程,无论在 2 个盘子的过程还是 64 个盘子的转移过程,都只涉及到 1 个盘子 ,那就是最大的那个盘子。因此,这个步骤是最容易完成的,我们先放在一边。
在 n 个盘子的转移过程中,A–>B 和 B–>C 这两个过程都涉及到 n-1 个盘子的转移;而你有没有发现,一旦转移的盘子数目超过 1, 这就可以看成一个独立的 汉诺塔问题。也就是说, A–>B 你完全可以看成是一个 A 柱上有 n-1 个盘子,B 柱 和 C 柱上没有盘子,最终要把 n-1 个盘子借助 C 柱,全部移动到 B 柱的汉诺塔问题,如下图所示。同样地,B–>C 过程可以看做将 n-1 个盘子从 B柱 借助 A 柱移动到 C柱的单独汉诺塔问题。
因此,我们把 A–>B 的过程和 B–>C 的过程用递归的方式来实现。
递归代码实现
#include<stdio.h>
void move(char start, char target) {
printf("%c-->%c\n", start, target);
}
// start 起始柱 A
// process 过程柱(辅助) B
// target 目标柱 C
void hanoi(int n, char start, char process, char target) {
if (n == 1) { // 只有一个盘子的时候,递归的出口,A --> C 柱
move(start, target);
}
else
{ // 这里起始柱还是 A,但是辅助柱变成 C了,目标柱变成 B了
hanoi(n - 1, start, target, process); // A-->B 柱用递归来完成,看做单独的汉诺塔问题
move(start, target); // A-->C 柱一步到位
hanoi(n - 1, process, start, target); // B--C 柱用递归来完成,看做单独的汉诺塔问题
// 这里起始柱变成 B,辅助柱变成 A,目标柱变成 C
}
}
void main() {
hanoi(2, 'A', 'B', 'C');
}