面试题 08.06. 汉诺塔问题
leetcode链接:https://leetcode.cn/problems/hanota-lcci/description/
在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。
一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。
移动圆盘时受到以下限制:
(1) 每次只能移动一个盘子;
(2) 盘子只能从柱子顶端滑出移到下一根柱子;
(3) 盘子只能叠在比它大的盘子上。
请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。
你需要原地修改栈。
示例1:
输入:A = [2, 1, 0], B = [], C = []
输出:C = [2, 1, 0]
示例2:
输入:A = [1, 0], B = [], C = []
输出:C = [1, 0]
提示:
A中盘子的数目不大于14个。
本文章主要围绕两个问题来展开:
1.为什么汉诺塔问题能使用递归?
2.如何解决汉诺塔问题?
为什么汉诺塔问题能使用递归?
可以使用递归的题目,可以总结为一句话:
解决大问题,可以使用相同类型的子问题
解决子问题 ,可以使用相同类型的子问题
我们来对汉诺塔问题进行分析:
当N = 1时,移动的策略:
直接将A中的盘子转移到C中
当N = 2时,移动的策略:
1.将A上面的第一个盘子移动到B上,
2.将A上面的第二个盘子移动到C上
3.将B上面的盘子移动到C上
当N = 3时,移动的策略:
1.将A上面的第一个盘子移动到C上
2.将A上面的第二个盘子移动到B上
3.将C上面的第一个盘子移动到B上
4.将A上面的第一个盘子移动到C上
5.将B上面的第一个盘子移动到A上
6.将B上面的第二个盘子移动到C上
7.将A上面的第一个盘子移动到C上
通过上面的演示,可以得到什么?
下面的三步
1.将A上面的第一个盘子移动到C上
2.将A上面的第二个盘子移动到B上
3.将C上面的第一个盘子移动到B上
当N = 3
时,前面的三步就是将A
中的N-1
个盘子借助C
移动到B
上,而这个操作,就是和N = 2时一模一样的。
而下面的这三步,
5.将B上面的第一个盘子移动到A上
6.将B上面的第二个盘子移动到C上
7.将A上面的第一个盘子移动到C上
当N = 3
时,这三步就是将B
中的N-1
个盘子借助A
移动到C
上,而这个操作,就是和N = 2时一模一样的。
因此,本问题的流程就抽象成什么了?
当解决N = 3时,可以用N = 2的策略 --> 当解决N个盘子的问题时,可以使用N - 1个盘子的策略。
而我前面提到的递归的本质:
解决大问题,可以使用相同类型的子问题
解决子问题 ,可以使用相同类型的子问题
如何写递归的代码??
写递归的代码,需要三个部分:
1.挖掘出重复的子问题 --> 函数头
2.只关心某一个子问题在做什么 --> 函数体
3.递归的出口 --> 防止死递归
1.挖掘出重复的子问题 --> 函数头
根据前面提到的n = 3时如何转化子问题的内容,将流程抽象出来,就变成了:
当N = 3
时,
第一步:将A中的N - 1个盘子借助C转移到B上 (重复的子问题)
第二步:将A中的最后一个盘子转移到C上
第三步: 将B中的N - 1个盘子借助A转移到C上 (重复的子问题)
我们根据重复的子问题设计出函数头。
从上面可以发现,有4个参数,分别是A,B,C和盘子的数量N
因此,函数头设计为:
2.只关心某一个子问题在做什么 --> 函数体
当N = 3
时,
第一步:将A中的N - 1个盘子借助C转移到B上 (重复的子问题)
第二步:将A中的最后一个盘子转移到C上
第三步: 将B中的N - 1个盘子借助A转移到C上 (重复的子问题)
我们要从宏观去思考,不要思考dfs
如何实现这个任务,把自己当成领导,相信dfs
一定能完成你布置的任务。
给dfs布置要给任务,相信dfs一定能完成任务 。
3.递归的出口 --> 防止死递归
n = 1时,不需要借助其他盘子,直接将A上的盘子转移到C上
代码:
class Solution {
public:
void hanota(vector<int>& a, vector<int>& b, vector<int>& c) {
dfs(a, b, c, a.size());
}
void dfs(vector<int>& a, vector<int>& b, vector<int>& c, int n)
{
if (n == 1)
{
//直接将a中的盘子转移到c上
c.push_back(a.back());
a.pop_back();
return;
}
//当有n个盘子的时候
dfs(a, c, b, n - 1); //将a中的n-1个盘子,借助c,转移到b上
//将a中的最后一个盘子转移到c上
c.push_back(a.back());
a.pop_back();
//将b中的n-1个盘子,借助a,转移到c上
dfs(b, a, c, n - 1);
}
};
总结
1.什么问题能使用递归?
解决大问题,可以使用相同类型的子问题
解决子问题 ,可以使用相同类型的子问题
2.如果要写递归,需要哪几步?
第一步:挖掘出相同的子问题 (关系到具体函数头的设计
第二步:只关心具体子问题做了什么 (关系到具体函数体怎么写,是一个宏观的过程)
第三步:找到递归的出口,防止死递归 (关系到如何跳出递归)