栈的引用简化了程序设计的问题,划分了不同的关注层次,使得思考范围缩小,更加聚焦于我们要解决问题的核心。
而数组等,要分散精力去思考数组的下标增减等细节问题,从而掩盖了问题的本质。
栈的应用 -- 递归
栈的重要应用就是在程序设计语言中实现了递归。
经典的递归例子: 斐波那契数列。
Eg: 如果说兔子出生两个月后便有繁殖能力,一对兔子每月能出生一对兔子,假如所有的兔子都不死,那一年后可以繁殖多少对兔子?
分析:我们拿新生的一对兔子从头分析:第一个月,兔子没有繁殖能力,所以还是一对;两个月后,生下一对兔子,共两对;三个月后,老兔子又生下一对,小兔子没有生育能力没有繁殖,则共有三对。。。 以此类推,可列出下表:
月数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
兔子对数 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 | 89 | 144 |
表中的数字1,1,2,3,5,8,13。。。构成了一个序列,该序列有明显的特点: 前面相邻两项之和,构成了后一项。
则可用数学含义来定义:
如需要列出40为斐波那契数列:
常规方法:
#include <stdio.h>
int main()
{
int i;
int a[40];
a[0] = 0;
a[1] = 1;
printf("%d",a[0]);
printf("%d",a[1]);
for(i = 2; i<40;i++)
{
a[i] = a[i-1]+a[i-2];
printf("%d",a[i]);
}
return 0;
}
递归方法:
int Fbi(int i)
{
if(i<2)
return i == 0 ? 0 : 1; //递归终止条件
return Fbi(i-1)+Fbi(i-2); //这里Fbi函数是自己,它在调用它自己
}
int main()
{
int i;
for( i = 0; i < 40;i++)
printf("%d",Fbi(i));
return 0;
}
函数如何可以自己调用自己?可以把递归调用中调用自己的函数看作是在调用另外一个函数,只不过这个函数的长相比较一样而已。
用5来模拟上面递归方法:
递归定义:
把一个直接调用自己或者通过一系列调用语句间接的调用自己的函数,称之为递归函数
然而,写递归程序最怕的就是陷入无穷无尽的递归调用中,所以,每个递归定义必须至少有一个条件(递归结束条件),满足时递归不在进行,即不在引用自身而是返回相应值退出。上例中 i< 2 便是递归的终止条件。
对比两种斐波那契数列,迭代和递归的区别是:
迭代使用的是循环结构,递归使用的是选择结构。 递归能使程序的结构更加清晰,简洁,更容易理解。但是大量的递归调用会建立大量的函数副本在内存中,其会消耗大量的时间和内存。 迭代则不需要反复的调用函数和占用额外内存。因此,我们应该视情况不同来选择不同的实线方法。
递归与栈的关系
在递归过程中,有前行和退回的阶段。递归过程的退回顺序是他前行顺序的逆序,在退回过程中,可能需要执行某种动作,包括恢复在前行过程中存储起来的某些数据。这种数据在存储中逆序恢复,以提供之后使用需求,则栈是这种结构的最佳选择。
简单来说,就是在前行阶段,对于每一层递归,函数的局部变量,参数值以及返回值都会被压入栈中。在退回的过程中,位于栈顶的局部变量,参数和返回值被弹出,用于返回调用层次中执行代码的其余部分,也就恢复了调用状态。