(可忽略)背景:目前,网上有很多关于汉诺塔求解的博文,我看了一些,感觉大致相同。首先都是圆盘移动的规律讲解了一下,然后贴上了解题代码。对递归的本质,没有进行深入的探究,可能也能解决当前的汉诺塔问题,但是给你一个新的递归类问题去解决,还是不行。在学习了imooc刘老师的课程后,对递归有了更清晰的认识,因此写此博文,总结递归类型问题的求解方法。希望看的人也可以有所收获,谢谢。
一 递归
本质上,递归是将原来的问题,转化为更小的同一问题
二 简单递归案例分析
此部分通过一个简单的案例:数组求和。来分析递归问题的求解过程,和递归函数一般的组成部分 (对递归很熟可以忽略,建议看)。
1 递归问题的解题思路分析
原问题:求解数组arr[0…n-1]中从索引0开始到索引n-1所有元素的和,即Sum(arr[0…n-1]);
将原问题转化为规模更小的同一问题,求从索引1开始到索引n-1所有元素的和,即Sum(arr[1…n-1])。然后用规模更小的问题的解去构建原问题的解,即
arr[0] + Sum(arr[1…n-1])。
上述过程一直重复,更小的同一问题,转化为更更小的同一问题,用更更小的同一问题去构建更小的同一问题的解。直到该问题不能被转化或分解成更小的问题,这里我们把该问题称为最基本的问题,即Sum([])
(总结)从上面递归问题的求解过程,我们可以得出解决递归问题的一般方法:找到最基本问题并给出基本问题的解;将原问题转换(或分解)成规模更小的同一问题,用规模更小的同一问题,去构建原问题的解。
2 代码编写的技巧
注意函数的“宏观”语义。即函数本身的功能,不要去过多的关注代码的执行过程!
以上都是刘老师相关课程的学习总结。下面正式开始用递归方式解决汉诺塔问题。
三 汉诺塔问题解决
1 问题描述
起源汉诺塔问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。
抽象为数学问题
如下图所示,从左到右有A、B、C三根柱子,其中A柱子上面有从小叠到大的n个圆盘,现要求将A柱子上的圆盘移到C柱子上去,期间只有一个原则:一次只能移到一个盘子且大盘子不能在小盘子上面,求移动的步骤和移动的次数。
A 代表源柱子,B代表辅助柱子,C代表目标柱子
问题描述参考链接
2 问题分析
原问题:将A上的n个圆盘,借助B,移动到C上(移动符合规则)我们使n等于3,来找出最基本的问题和规模更小的同一问题。
从图中可以看出,原问题被分解成了三个更小的同一问题。
第一个更小的同一问题作用:把除最后一个圆盘之上的所有圆盘,从A上,借助C,移动到B上
第二个更小的同一问题作用:把最后一个圆盘,从A上,借助B,移动到C上
第三个更小的同一问题作用:把所有圆盘,从B上,借助A,移动到C上
至此,我们把原问题分解成了三个更小的同一问题,并利用三个更小的同一问题的解(更小的同一问题的分解过程和原问题的分解过程相同)构建出了原问题的解。
最基本的问题(即该问题不同被分解成更小的同一问题)显然是一个圆盘的移动;
(1)到(7)的过程即实际解题时的移动过程,可以自己测试下。
3 问题解决
根据在第二部分分析出的最基本问题和更小的同一问题及如何使用更小的同一问题构建原问题解,代码其实很简单了。注意关注代码的宏观语义,即函数的功能,不要太过纠结函数的执行过程。/**
* 把source上的n个圆盘,借助assist,移动到destination上
* @param source 源
* @param assist 辅助
* @param destination 目标
* @param n 移动的数量
*/
public static void move( char source, char assist, char destination, int n){
// 最基本问题的解
if(n == 1){
System.out.println(source + "--->" + destination);
return;
}
// 把原问题转化为更小的同一问题,然后用更小的同一问题的解构建原问题的解
move(source, destination, assist, n - 1);
move(source, assist, destination, 1);
move(assist, source, destination, n - 1);
}
// 测试函数
public static void main(String[] args) {
move('A', 'B', 'C', 3);
}
执行结果如下,和我们在第二部分分析的过程相同。
4 代码调试
本部分主要介绍,递归函数在微观层面是如何执行的。方法主要有两个:1 在程序代码中添加执行相关的日志
2 通过较小的问题规模,使用纸笔或其他工具描画程序的执行过程
(1) 在程序代码中添加执行相关的日志
主要说明清楚函数的执行目标及每一步的作用,示例代码如下 /**
* 把source上的count个圆盘,借助assist,移动到destination上
* @param source 源
* @param assist 辅助
* @param destination 目标
* @param n 移动的数量
* @depth 递归深度,表示当前执行的递归函数位于那一层
*/
public static void move( char source, char assist, char destination, int n, int depth){
String depthString = generateDepthString(depth);
// 首先,说明函数的执行目标
System.out.print(depthString);
System.out.println(String.format("Call: move %d disk(s) from %c to %c with the help of %c.", n, source, destination, assist));
// 最基本问题的解
if(n == 1){
// 每一步的作用
System.out.print(depthString);
System.out.println(source + "--->" + destination);
return;
}
// 把原问题转化为更小的同一问题,然后用更小的同一问题的解构建原问题的解
// 每一步的作用
System.out.print(depthString);
System.out.println(String.format("Call: move %d disk(s) from %c to %c with the help of %c."
, n - 1, source, assist, destination));
move(source, destination, assist, n - 1, depth + 1);
System.out.print(depthString);
System.out.println(String.format("Call: move %d disk(s) from %c to %c with the help of %c."
, 1, source, destination, assist));
move(source, assist, destination, 1, depth + 1);
System.out.print(depthString);
System.out.println(String.format("Call: move %d disk(s) from %c to %c with the help of %c."
, n - 1, assist, destination, source));
move(assist, source, destination, n - 1, depth + 1);
}
/**
* 生成代表递归函数深度的字符串,第一层即 "--".
* @param depth
* @return
*/
private static String generateDepthString(int depth){
StringBuilder res = new StringBuilder();
for(int i = 0; i < depth; i ++){
res.append("--");
}
return res.toString();
}
// 测试函数
public static void main(String[] args) {
move('A', 'B', 'C', 3, 0);
}
日志输入如下图所示: 函数第一次被调用,初始化函数处于第0层,在第0层递归调用,进入新的函数执行,函数处于第1层,以此类推。在什么时候进入下一层子函数执行,什么时候从子函数退出到上一层,从打印的日志可以很清晰的得知。
本文持续更新中... 觉得有收获的,可以给个赞,谢谢。