目录
[n = 3时](#n = 3时)
递归核心
递归:函数自己调用自己 两个必备条件(缺一不可,否则无限栈溢出)
基线条件(终止条件):满足条件直接返回,不再递归调用,防止死递归。
递归条件 :把原问题拆解成规模更小、结构相同的子问题。
大事化小,直到最小可直接求解的情况,再逐层返回合并结果。
思想关键
① 不要试图一层一层跟踪全部调用!
我认为最关键的一条,就是不要去质疑自己写的递归(或许莫名其妙地就写对了)
误区:手动去模拟每一层递归的执行,栈深一大就混乱。
正确思维:相信递归函数是正确的(数学归纳法思想)。
假设 f(n-1) 能正确完成任务,我只要写好 f(n) 和 f(n-1) 的关系即可。
汉诺塔:相信 hanoi (n-1, ...) 可以把 n-1 个盘子完整挪好,不用关心它内部怎么挪。
② 递归分为两个阶段:递、归
- 递(向下):不断拆分问题,函数不断调用自身,压入栈,直到碰到基线条件。此时还没有返回结果。
- 归(向上回溯):到达基线,开始返回;子问题算出结果,逐层向上合并得到大问题答案。
阶乘:递:fact (4)→fact (3)→fact (2)→fact (1);归:1→2→6→24
③ 递归的本质:利用函数调用栈保存中间状态
每一次递归调用,都会产生独立的局部变量,保存在栈帧里。 回溯的时候,自动恢复上一层函数的上下文。
汉诺塔打印移动步骤,就是回溯 / 递过程中输出状态。
示例
阶乘
公式:

cpp
#include <stdio.h>
using namespace std;
int fact(int n)
{
if (n == 1 || n == 0) // 基线条件
return 1;
return n * fact(n - 1); // 递归:拆成更小子问题
}
int main()
{
cout << fact(4);
return 0;
}
调用过程
fact(4):
fact(4)=4*fact(3) →
fact(3)=3*fact(2) →
fact(2)=2*fact(1) →
fact(1)=1
然后回溯:2*1=2 → 3*2=6 → 4*6=24
斐波那契数列
公式:

cpp
#include <stdio.h>
#include <iostream>
using namespace std;
int fib(int n)
{
if (n == 1 || n == 2)
return 1;
return fib(n - 1) + fib(n - 2);
}
int main()
{
cout << fib(4);
return 0;
}
缺点:大量重复计算,效率低。
汉诺塔
今天尝试了在知道要用递归的前提下,试着写了汉诺塔,结果莫名其妙地写对了。
很开心。之后又花了一小下午弄懂其中的原理。
题目/规则:
起始有3 根柱子,一般命名:A(源柱)、B(辅助柱)、C(目标柱)
有n 个大小互不相同的圆盘 ,一开始全部叠在 A 柱,大盘在下,小盘在上,依次堆叠。
移动规则
一次只能移动 1 个圆盘
圆盘只能从一根柱子顶部拿,放到另一根柱子顶部
任何时刻,不能把大盘放在小盘上面
目标:把全部 n 个圆盘,从 A 柱完整移动到 C 柱,移动过程遵守上面规则。
思考
普通的线性运算很难实现,用递归最好写;
以三个(n = 3)为例:

找基线条件
首先想基线条件:
当n = 1时,将1号盘从当前柱移动到目标柱,并且返回,完成"归"。
注意:这里我没有说从A柱移动到C柱。
因为在递归中,你无法确定1号盘当前在哪个柱子,也不能确定1号盘去哪个柱子。
疑问:为什么第一步1号一定要去C
或许你会问:只有一个盘的时候肯定要A -> C,那多个盘的时候,1号盘不能现A -> B?
结论是确实可以,但不是最优解了。
可以这么理解:如果一个柱子的顶端是1号盘。那这个柱子就"死了"------

此时这个柱子不能再移入任何的盘子
若一开始就将1号盘移动到B柱,2号会去C,想要再操作只能进行1号盘B -> C


但这样已经gameover了------两个最小的在C柱上,已经不可能实现最短路径了。
思考怎么"递"
现在不以三个为例了,个数太少容易使人想走"捷径"
假如现在有八个盘:

先写个函数模板:
cpp
void hanruo(int n, char ori, char buf, char dest)
n为个数,ori为柱当前所在的 ,buf为当前的缓存柱子,dest是当前目标移动的柱。
注意:以上的参数只是针对当前状态,这对理解特别重要------
因为递归中谁做缓冲柱,谁做目标柱都不一定
当然第一层的ori buf dest就分别是A B C
看最外层:当n = 8输入进函数后,在写入基线条件后,要思考面对这个八层的塔,要干什么?
我们想要把1~7层全部挪到B柱,来解放8号盘------将其移动到C;


这是将8号盘移动到C的唯一方式,所以也其他没异议
于是就开始递归:
cpp
void hanruo(int n, char ori, char buf, char dest)
{//ori -> A buf -> B dest -> dest
if (n == 1)
{
printf("%d号盘从%c移动到%c\n", n, ori, dest);
return;
}
hanruo(n - 1, ori, dest, buf);
hanruo(n - 1, ori, dest, buf);
这一句,就表示:将n - 1,也就是前七层,从ori(A),以dest(C)为缓冲,移动到buf(B)
也就实现了我们想要的效果
疑问:怎么就实现了?
这也没写啥具体的代码,怎么就实现这个功能了?
确实,我们代码还没写全,逻辑还没闭环,不了解也没事,接着看就好了。
之后,就要将8号盘移动到C
cpp
void hanruo(int n, char ori, char buf, char dest)
{
if (n == 1)
{
printf("%d号盘从%c移动到%c\n", n, ori, dest);
return;
}
hanruo(n - 1, ori, dest, buf);
printf("%d号盘从%c移动到%c\n", n, ori, dest);
对,就加了一句话
printf("%d号盘从%c移动到%c\n", n, ori, dest);
足以表示这个过程

那之后呢,这C柱也被"占领了",咋办?
其实C柱没有"被占"------C柱有最大号的盘子,意味着这个柱可以执行任何操作------不会受到阻碍
那我们就直接把C柱子看成空的就好了。

那是不是和这个情况很像?只不过此时满的应该是B 而不是A

那不就是初始n = 7的样子吗?只是缓存柱子不一样了,那就可以写
cpp
void hanruo(int n, char ori, char buf, char dest)
{
if (n == 1)
{
printf("%d号盘从%c移动到%c\n", n, ori, dest);
return;
}
hanruo(n - 1, ori, dest, buf);
printf("%d号盘从%c移动到%c\n", n, ori, dest);
hanruo(n - 1, buf, ori, dest);
}
以ori(A)为缓冲柱,将盘子从buf(B)移动到dest(C)
好了,已经写完了,下面就交给递归吧。
或许你很惊讶------这么短?好吧,其实我一开始也很惊讶------我压根没打算这点代码可以跑起来
但结果确实是对的,我很开心
cpp
#include <stdio.h>
#include <iostream>
using namespace std;
void hanruo(int n, char ori, char buf, char dest)
{
if (n == 1)
{
printf("%d号盘从%c移动到%c\n", n, ori, dest);
return;
}
hanruo(n - 1, ori, dest, buf);
printf("%d号盘从%c移动到%c\n", n, ori, dest);
hanruo(n - 1, buf, ori, dest);
}
int main()
{
int n;
scanf("%d",&n);
hanruo(n, 'A', 'B', 'C');
}
过一遍
再想一遍一套流程中都干了什么
第 1 步:hanruo(n - 1, ori, dest, buf);
把 ori 上面的 n-1 个小盘,借助 dest,搬到 buf。
重点: 现在要移动的是 n-1 个盘子,所以三个柱子角色发生切换:
- 起点依旧是
ori - 辅助柱变成了
dest - 目标柱变成了
buf
效果:执行完这一步之后,最大的第 n 号盘子,孤零零留在 ori 上;上面 n-1 个小盘全部挪到 buf。 此时 ori 只剩最大盘,下面是空的,可以移动大盘。
第 2 步:printf("%d号盘从%c移动到%c\n", n, ori, dest);
移动第 n 号最大盘子,从 ori → dest。
这一步是当前层真正做的操作,递归调用只是安排小盘。 大盘没有任何盘子压着,目标柱 dest 现在是空(或者上面盘子都比 n 号大,满足规则),直接移动。
执行完:最大盘子已经就位,固定在 dest 底部,之后再也不动它。
第 3 步:hanruo(n - 1, buf, ori, dest);
把 buf 上存放的 n-1 个小盘,借助 ori,搬到 dest。
角色再次切换:
- 起点现在是
buf(n-1 个小盘现在在这里) - 辅助柱变成
ori(现在只有那个已经移走大盘的空柱子) - 目标柱是
dest(最大盘已经放在这里,小盘最后叠上去)
效果:n-1 个小盘全部移动到 dest,叠在第 n 号大盘上面。 至此,n 个盘子全部从 ori 移到 dest,任务完成。
对的,这已经是一套完美的逻辑,所以可以运行起来。
手动演绎递归过程
虽然我说得头头是道,但我实际上了对这个递归的实际操作不很熟悉。
所以我决定拿几个比较小的数来演绎一下过程
不过,一开始写的时候千万不要自己演绎,容易把自己绕进去。要靠逻辑与"感觉"。
n = 3时

接着开始递归:


↓

再"归"↓

↓



↓

此时1 2的进程都关闭了,再继续3的进程:

↓










这就演示完了,虽然我的图可能不是很美观,没办法,太难画了。
通过这个流程,希望可以加深你的理解。