栈的应用之递归

63 阅读5分钟

栈和递归是息息相关的,所谓递归其核心思想就是将一个问题划分为子问题解决,然后再把子问题划分为更小的问题,直至划分到不能再划分为止,因此构成递归的条件有

  • 可以把待解决的问题转化为一个新问题,而这个新的问题的解决方法仍与原来的解决方法相同,只是所处理的对象有规律地递增或递减
  • 必定要有一个明确的结束递归的条件

先来看看函数调用背后的过程:

函数调用的过程

比如main中调用了func1,然后func1里面调用func2......

图片.png

从这里看出函数调用的过程特点如下:

函数调用的特点:最后被调用的函数最先执行结束(LIFO)

符合栈的定义,所以函数调用时,需要用一个栈存储: ① 调用返回地址 ② 实参 ③ 局部变量

图片.png

  • 比如func2:#2 是func2的调用返回地址,即func1,实参x,局部变量mn

栈在递归中的应用举例

适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题


比如: 求阶乘,求斐波那契数列

图片.png

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);
}

递归调用时,函数调用栈可称为“递归工作栈” 每进入一层递归,就将递归调用所需信息压入栈顶, 每退出一层递归,就从栈顶弹出相应信息

图片.png

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);

图片.png

图片.png


上面是举二个算法例子,说明递归使用栈时候的表示过程。

接下来,我们会用各个方面来阐述,从时间复杂度最高的递归,再到如何去优化递归以及如何非递归


栈的递归应用

暴力递归

  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)重复后导致时间长。


我们知道递归问题本质就是二叉树的问题,所以在递归的过程中就是在遍历一颗二叉树,所以这种接法对应的递归树是这样的

图片.png

很显然,想要计算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);
    }

这种算法的递归树如下,你可以很明显的发现,这种带备忘录的解法就是我们经常说的“剪枝”操作,把一颗非常冗余的树修剪的很干净,或者说就是减少了重叠子问题

图片.png

很明显它的时间复杂度要低


自底向上——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(动态规划)数组:参考


总结

  • 适合用“递归”算法解决:可以把原始问题转换为属性相同,但规模较小的问题

  • 符合栈的定义,所以函数调用时,需要用一个栈存储: ① 调用返回地址 ② 实参 ③ 局部变量

  • 递归调用时,函数调用栈可称为“递归工作栈” 每进入一层递归,就将递归调用所需信息压入栈顶, 每退出一层递归,就从栈顶弹出相应信息