@栈与递归(其实更多的是解析递归)
一.递归
1.概念
什么是递归,就是高中数学上我们学的那个,递推式。当然,如果把一个递推公式用函数来表示,那么我们可以叫这个函数递归函数
。递归函数
是一个直接调用自己或通过一系列的语句简介地调用自己的函数,称作递归函数。
比如我们的阶乘函数:
F
a
c
t
(
n
)
=
{
1
,
n
=
0
n
⋅
F
a
c
t
(
n
−
1
)
,
n
>
0
Fact(n)=\left\{ \begin{aligned} 1,n=0\\ n\cdot Fact(n-1),n>0\\ \end{aligned} \right.
Fact(n)={1,n=0n⋅Fact(n−1),n>0
亦或者我们比较熟悉的斐波那契数列:
F
i
b
(
n
)
=
{
0
,
n
=
0
1
,
n
=
1
F
i
b
(
n
−
1
)
⋅
F
i
b
(
n
−
2
)
,
n
>
1
Fib(n)=\left\{ \begin{aligned} 0,n=0\\ 1,n=1\\ Fib(n-1)\cdot Fib(n-2),n>1\\ \end{aligned} \right.
Fib(n)=⎩⎪⎨⎪⎧0,n=01,n=1Fib(n−1)⋅Fib(n−2),n>1
2.实现
我们可以把那些常数项,叫做递归边界
,比如阶乘的f(n)=1,n=0
或者斐波那契数列的f(n)=0,n=0
,f(n)=1,n=1
。
那我们写段实现斐波那契数列的递归代码:
int Fib(int n)
{
if (n == 0) return 0;
else if (n == 1) return 1;
else
{
return Fib(n-1) + Fib(n-2);
}
}
int main()
{
printf("%d", Fib(10));
return 0;
}
只要我们能写出递归的函数表达式,就可以写出它的递归函数。个人认为:递归边界是最重要的。如果递归边界没有处理好,那么递归的可能永远持续下去,然后爆栈而死。
3.较难的递归分析——汉诺塔
汉诺塔的游戏规则就是:把A中的盘子移动到C盘子上,大盘子不能放在小盘子上。
对于这个我们多试几次:
-
n=1
第1次 1号盘 A---->C
-
n=2
第1次 1号盘 A---->B
第2次 2号盘 A---->C
第3次 1号盘 B---->C
-
n=3
第1次 1号盘 A---->C
第2次 2号盘 A---->B
第3次 1号盘 C---->B
第4次 3号盘 A---->C
第5次 1号盘 B---->A
第6次 2号盘 B---->C
第7次 1号盘 A---->C
如果我们用整体法的思想来看,如果把一堆盘子从A移动到C,那么我们可以把A柱子上的盘子看成两个整体,最下面的盘子和它上面的所有盘子。那么我们的过程就是:
- 上面所有的盘子从A到B
- 最下面的盘子从A到C
- 上面所有的盘子从B到C
那么我们如何把最上面的盘子移动到B和C呢?那就把这部分盘子看成两部分,最下面一张盘子和上面所有的盘子。
如果移动到B,那么C就是我们的辅助塔(因为游戏是从A移动到C,所以B是辅助塔),所以我们就对这上面的部分执行“A到B的汉诺塔操作”。
反之,从B移动到C的话,A就是辅助塔,所以我们执行的是"B到C的焊诺特操作"
开始套娃。直到最后我们只有一个盘子,那么狠显然,它执行“A到C的汉诺塔操作”且只有一块盘子,那就把它移动到C。
那么我们的递归表达式就出来了:
递归边界: n=1时 把1号盘子 A→C
递归内容:①把n-1号盘子 A→B
②把n号盘子 A→C
③把n-1号盘子 B→C
好了,代码实现一下:
void hano(int n, char a, char b, char c) // hano(n=有几个碟子,a=主塔,b=辅助塔,c=目标塔)
{
if (n == 1) printf("No.%d %c -> %c\n", 1, a, c);
else
{
hano(n - 1, a, c, b); // 这里是A to B的操作,C是辅助塔
printf("No.%d %c -> %c\n", n, a, c);
hano(n - 1, b, a, c); // 这里是B to C的操作,A是辅助塔
}
}
int main()
{
hano(3, 'A', 'B', 'C'); // 这里是A to C的操作
return 0;
}
二.栈与递归的关系
在高级语言中,如果需要调用(这是动词)函数,那么调用(名词)函数
和被调用函数
之间的链接与信息交换需要通过内部的栈进行。
在运行被调用函数之前,系统会完成三件事:
- 把所有的实参、返回地址等信息传递给被调用函数保存。
- 为被调用函数的局部变量分配储存区
- 将控制转移到被调用函数的入口。
然后在被调用函数返回调用函数之前,我们也要完成三件事:
- 保存被调函数的计算结果
- 释放被调用函数的数据区
- 依照被调用函数保存的返回地址将控制转移到调用函数。
假设我们像递归这样成嵌套的调用时,那么先调用的函数肯定要最后返回,所以在编译器的内部,是通过栈的方式为我们保存这些关系与信息。
所以我们的递归之间的信息传递需要使用到栈,幸好编译器会帮我们管理递归工作栈
,所以我们只需要好好考虑递归边界和递归的内容就可以啦!