函数调用栈和递归函数分析以及尾递归的讲解

  1. 什么是调用栈?作用是什么

调用栈(执行栈)

在Windows 等大部分操作系统中, 每个运行中的二进制程序都配有一个调用栈( call stack ) 或执行栈( execution stack ) 。

调用栈的作用首先在于跟踪属于同一程序的所有函数, 记录它们之间的相互调用, 以保证被调用的函数执行完毕后可以准确地返回调用函数。

调用栈以帧( frame )作为基本单位。每次函数调用时, 都会相应地创建一帧, 记录其在二进制程序中的返回地址( return address ) , 井将该帧(地址)压人调用栈,若在该函数返回之前又发生新的调用, 则同样地要将与新函数对应的一帧压人栈中, 成为新的栈顶,函数一旦运行完毕, 对应的帧随即弹出, 操作系统将把运行控制权交还给该函数的上层调用函数,并按照该帧中记录的返回地址确定在二进制程序中继续执行的位置。

因此, 在任一时刻调用栈中的各帧,分别对应于那些己被调用但尚未返回的函数实例,称作这时刻的活跃函数实例( active function instance ) ,只有一个。特别地,位于栈底的那帧必然对应于程序运行的入口主函数main() , 它从调用栈中弹出意味着整个程序的运行结束,此后控制权将交还给操作系统。

递归

  1. 递归和函数调用的关系?递归只有一个函数,怎么对应上调用栈
  2. 怎么避免递归无休止?
  3. 递归函数都可以用循环的方式表达吗?哪种逻辑清晰?
  4. 栈溢出是什么,在递归中使怎么体现出来的?

递归的理解

递归既可等效地理解为函数的自我调用

可以将递归实例视作一般的函数调用,井在调用栈中为其创建对应的一帧。 如此,同一个函数可能同时拥有多个实例, 并在调用栈中分别存有一帧。当然,即便是同一函数的不同实例,在各自的帧中对同名的参数或变量都有独立的副本, 故其数值不尽相同。

递归的注意事项

  1. 一定要有平凡情况称递归基,出现递归基时,可以结束算法
  2. 防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出

  3. 用好递归,要时刻明白函数是干什么用的!!!并且注意每一帧函数值和传入参数的改变

递归的分类

线性递归,二分递归,多分支递归等

递归的例子

  • 汉诺塔移动问题:a盘上有n个盘子(上小下大),全部移动到c盘上,大盘在下,小盘在上

思路:

  1. 仍然是倒着考虑问题,现将a盘最下面的盘子放到目标c上,通过递归很容易实现!
  2. 步骤1之后,a盘空,b盘有n-1个盘子!
def hannotaMove(n,a,b,c):
    if n == 1:
        print("move",a,'-->',c) #递归基(非递归处理)base case of recursion,,可能不止一个
    else:
        hannotaMove(n-1,a,c,b) 
        hannotaMove(1,a,b,c)
        hannotaMove(n-1,b,a,c)
hannotaMove(4,'A','B','C')
  • 利用不同方法的递归,计算Fibonacci数列的第n项:输出Fibonacci数列的第n项

1.二分递归版

int fib(int n)//O(2^n):指数复杂度,实际中无意义
{
    //若到达递归基,直接取值
    return (2>n)?(int)n:fib(n-1)+fib(n-2);
}

2.线性递归版:改进

为什么会改进?因为将每次计算的结果存了起来,不用重复计算了

int fib(int n, int &prev)//prev辅助变量记录前一项,复杂度O(n)
{
    if(n == 0)
    {
        prev = 1;
        return 0;
    }
    else
    {
        int prevPrev;
        prev = fib(n-1,prevPrev);//递归计算前两项
        return prevPrev+prev;
    }
}

3.动态规划版(循环问题用while往往起到意想不到的效果):借助少量的辅助空间,记录子问题的解答 

动态规划的思想:

  • 把原始问题划分为一系列子问题
  • 求解每个子问题仅一次,并将其结果保存在一个表中,以后用到时到时直接存取,不重复计算,节省计算时间
  • 自底向上地计算
int fibI(int n) //O(n)
{
    int f=1,g=0; //初始化fib(1)=1,fib(0)=0
    while(0 < n--)
    {
        int temp=g;
        g+=f;
        f=temp;
    }
    return g;
}

  尾递归的优点

来源:https://blog.csdn.net/zcyzsy/article/details/77151709

普通递归需要压栈和出栈,时间空间消耗大;尾递归则没有(可以通过编译器优化);

解释:

如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

原理:

当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活动记录而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际的运行效率会变得更高。

以尾递归方式实现阶乘函数的实现:

int facttail(int n, int res)
{
    if (n < 0)
        return 0;
    else if(n == 0)
        return 1;
    else if(n == 1)
        return res;
    else
        return facttail(n - 1, n *res);
}

res(初始化为1)维护递归层次的深度。这就让我们避免了每次还需要将返回值再乘以n。然而,在每次递归调用中,令res=n*res并且n=n-1。继续递归调用,直到n=1,这满足结束条件,此时直接返回res即可。

其实最精髓就是 通过参数传递结果,达到不压栈的目的

猜你喜欢

转载自blog.csdn.net/vict_wang/article/details/81635963
今日推荐