首先,让我们了解一个什么是汉诺塔问题
汉诺塔,该问题的主要材料包括三根高度相同的柱子和一些大小及颜色不同的圆盘,三根柱子分别为起始柱A、辅助柱B及目标柱C。
游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。
分析:
对于这样一个问题,我们可能不能直接写出移动盘子的每一步,但我们可以利用下面的方法来解决。设移动盘子数为n,为了将这n个盘子从A杆移动到C杆,可以做以下三步:
(1)以C杆为中介,从A杆将1至n-1号盘移至B杆;
(2)将A杆中剩下的第n号盘移至C杆;
(3)以A杆为中介;从B杆将1至n-1号盘移至C杆。
同时这个思维也是我们编写代码的思维。
我们这次从八个角度来分析:
1.自顶向下,逐步求精
要将n个盘子移到C柱,首先要把n-1个盘子移动到B柱,再将第n个盘子移动到C柱,最后再把n-1个盘子移动到C柱,之后的问题就是如何把n-1盘子从A移到B上,然后一个个问题递归下去,解决这个问题,就是我们要说的自顶向下,逐步求精。
2.不要跨层分析
不要跨层分析说的就是,加入我们在处理将n-1个盘子和第n个盘子的移动的时候,就不要思考, n-1个盘子是如何移动到到B柱的,我们只需要知道n-1个盘子是要移动到B柱的就好,具体是如何达成的呢,就不需要我们这一步去考虑。
3.递归和分治
我们编写调用一个函数来解决汉诺塔问题,就是将n个盘子的移动分为n-1个和第n个盘子的移动,将原先的问题分成这两个问题,之后hanoi()会把n-1个盘子的移动递归完成,我们就将原问题转化为了两个子问题。
4.形参与实参
原本形参与实参的使用是在函数的学习中第一次使用,hanoi函数在编写时,会在括号里使用一些形参,就是函数表面上使用的名称,就是“原先柱”辅助柱“”目标柱”这些名字的使用,但在实际上,假如需要移动的只是一个盘子,他的原先柱就会是‘A’,目标柱就会是‘C’,在这里,‘A’”C‘就是这个函数的实参,就是实际函数需要输入的值。
5.标识符
在这个hanoi函数的编写里,要使用有意义、规范的标识符,使用A、B、C来表示在汉诺塔问题盘子移动时需要经过的柱子,在函数标写形参时,使用的Source、Destination、Transit,可以让后续的使用更加的清晰,让代码更加规范。
6.时间复杂度
时间复杂度是在之前学过的内容,在汉诺塔问题中,他的时间复杂度是O(2^n),在每次函数的使用时,我们需要两个的调用,在递归时,有n次的递减,这样,一次hanoi函数的调用,就会有2^n次的计算,所以,汉诺塔问题的时间复杂度是O(2^n).
7.递归栈(代码跟踪)
递归函数的调用过程需要使用递归栈来保存每个函数调用的状态。每一次hanoi函数的调用,都会建立一个新的函数调用栈,并让其压栈,递归调用返回时,又会弹出,恢复其之前调用时候的状态。
8.空间复杂度
汉诺塔问题的空间复杂度时O(n),在汉诺塔问题里面会有n个盘子,每一个值的改变都会有压栈,一共有n个盘子,所以其空间复杂度时O(n)。
总之,汉诺塔问题涉及到自顶向下的逐渐求精、函数调用、递归和分治。通过合理地使用形参和实参、有意义、规范的标识符和递归栈,我们可以解决这个问题,并计算出它的时间复杂度和空间复杂度。
代码实现
void hanoi(int paraN, char paraSource, char paraDestination, char paraTransit) {
if (paraN <= 0) {
return;
} else {
hanoi(paraN - 1, paraSource, paraTransit, paraDestination);
printf("%c -> %c \r\n", paraSource, paraDestination);
hanoi(paraN - 1, paraTransit, paraDestination, paraSource);
}
}
流程图表示