栈和递归是息息相关的,所谓递归其核心思想就是将一个问题划分为子问题解决,然后再把子问题划分为更小的问题,直至划分到不能再划分为止,因此构成递归的条件有
- 可以把待解决的问题转化为一个新问题,而这个新的问题的解决方法仍与原来的解决方法相同,只是所处理的对象有规律地递增或递减
- 必定要有一个明确的结束递归的条件
先来看看函数调用背后的过程:
函数调用的过程
比如main中调用了func1,然后func1里面调用func2......
从这里看出函数调用的过程特点如下:
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
符合栈的定义,所以函数调用时,需要用一个栈存储: ① 调用返回地址 ② 实参 ③ 局部变量
- 比如func2:#2 是func2的调用返回地址,即func1,实参x,局部变量mn
栈在递归中的应用举例
适合用“递归”算法
解决:可以把原始问题转换为属性相同
,但规模较小
的问题
比如: 求阶乘,求斐波那契数列
Eg1:递归算法求阶乘
//计算正整数n!
int factorial(int n){
if(n==0||n==1)
return 1;
else
return n*factorial(n-1);
}
int main(){
//....其他代码
int x=factorial(10);
}
递归调用时,函数调用栈可称为“递归工作栈
” 每进入一层递归
,就将递归调用所需信息压入栈顶
, 每退出一层递归
,就从栈顶弹出相应信息
。
Eg2: 递归算法求斐波那契数列
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(){
//...其他代码
int x=Fib(4);
上面是举二个算法例子,说明递归使用栈时候的表示过程。
接下来,我们会用各个方面来阐述,从时间复杂度最高的递归,再到如何去优化递归以及如何非递归
栈的递归应用
暴力递归
int fib(int n)
{
if (n==0) return 0;
if (n==1 || n==2) return 1;
return fib(n-1)+fib(n-2);
}
就是直接递归调用函数,跟上面的例子一样
时间复杂度为0(n)
为什么这么慢呢?其实这就是递归的本质——划分子问题
,只不过斐波那契数列中存在着大量不需要重复进行的子问题,Fib(n-1)+Fib(n-2)重复后导致时间长。
我们知道递归问题本质就是二叉树的问题,所以在递归的过程中就是在遍历一颗二叉树,所以这种接法对应的递归树是这样的
很显然,想要计算fib(20)
,就先要计算fib(19)
和fib(18)
,计算fib(19)
又要计算fib(18)
和fib(17)
…可以看出图中的f(18)
,f(17)
很显然是不需要计算的,虽然图中只画出了一点,但是你应该明白,这颗递归树要是完全展开,那是很恐怖的。所以占用时间多
带有备忘录的栈递归算法
既然耗时的原因是因为重叠子问题太多,那么不如这样:把fib(18)
计算完之后,存储在一个备忘录中,下次需要求解fib(18)
,直接从备忘录里面拿多好。
而实现备忘录,我们更多用数组,当然你也可以用哈希表,道理是一样的。
int back(int &memory,int n)
{
if(n==1 || n==2) return 1;
if(memory[n]!=0) return memory[n];//一旦对应位置不等于0,表示已经计算过了,立马直接返回结果即可
return back(memory,n-1)+back(memory,n-2);//否则是没计算过这个位置的,计算返回
}
int fib(int n)
{
if (n==0) return 0;
int memory(n+1,0);//设立一个数组备忘录,初始状态设置为0,0表示该位置的元素没有被记录在备忘录上
return back(memory,n);
}
这种算法的递归树如下,你可以很明显的发现,这种带备忘录的解法就是我们经常说的“剪枝”操作,把一颗非常冗余的树修剪的很干净,或者说就是减少了重叠子问题
很明显它的时间复杂度要低
自底向上——dp数组解法
我们把上面的备忘录变成一个dp数组,通过dp数组不断迭代向上求解。
int fib(int n)
{
if(n==0) return 0;
if(n==1 || n==2) return 1;
int dp(n+1,0);
//最简单的情况,base case
dp[1]=1;
dp[2]=1;
for(int i=3;i<=n;i++)
{
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
其效率很高
上面那种算法时间复杂度很低,但是空间复杂度却很高。但是我们发现这个数列中,当前状态仅仅和前两个状态有关,与前面无关,所以我们可以优化空间,不采用数组,直接两个变量,交替保存。
int fib(int n)
{
if (n==0) return 0;
if (n==1 || n==2) return 1;
int prev=1;int curr=1;
for(int i=3;i<=n;i++)
{
int sum=prev+curr;
prev=curr;
curr=sum;
}
return curr;
}
关于dp(动态规划)数组:参考
总结
-
适合用
“递归”算法
解决:可以把原始问题转换为属性相同
,但规模较小
的问题 -
符合栈的定义,所以函数调用时,需要用一个栈存储:
① 调用返回地址 ② 实参 ③ 局部变量
-
递归调用时,函数调用栈可称为“
递归工作栈
” 每进入一层递归
,就将递归调用所需信息压入栈顶
, 每退出一层递归
,就从栈顶弹出相应信息
。