美文网首页
递归、调用栈及尾递归

递归、调用栈及尾递归

作者: 908c5d752fc4 | 来源:发表于2019-05-15 18:19 被阅读0次

1.递归与栈

无论是否递归调用,当在一个函数(外层函数)的运行期间调用另一个函数(被调用函数,即内层函数)时,在运行被调用函数之前,系统需要先完成3个操作,即:

  • 将所有的实参、返回地址等信息传递给被调函数保存;
  • 为被调函数的局部变量分配存储区;
  • 将控制转移到被调函数的入口。

从被调函数返回调用函数(外层函数)之前,系统还要完成3个操作,即:

  • 保存被调函数的计算结果;
  • 释放被调函数的数据区;
  • 依照被调函数保存的地址将控制权转移到调用函数(外层函数)。

当有多个函数构成嵌套调用时,按照"后调用先返回"的原则,上述函数之间的信息传递和控制转移必须通过"栈"来实现,每当调用一个函数时,就在栈顶为它分配一个存储区,每当退出一个函数时,就释放它的存储区,当前正在运行的函数的数据区必在栈顶。递归函数的运行过程类似于多个函数的嵌套调用,只是调用和被调用函数是同一个函数。

函数调用时,需要在栈中分配新的帧,将返回地址调用参数局部变量入栈。所以递归调用越深,占用的栈空间越多。

假设n = 3,该递归的执行步骤,即圧栈、出栈的说明如下:

function recursive(int n) {
  if(n == 1) return 1;

  doSomething();
  recursive(n-1);
  doOtherThing();

}

递归与调用栈

2.尾递归

为了解决递归的开销大问题,使用尾递归优化,具体分两步:

1)编码时,把递归调用的形式写成尾递归的形式;
2)编译时,编译器碰到尾递归,自动按照某种特定的方式进行优化编译

使用尾递归的好处:因为进入下一层函数不再需要参考外层函数的信息,因此没必要保存外层函数的栈信息,递归需要用的stack只有目前这层被调用函数的,因此避免了栈溢出风险。
一些语言提供了尾递归优化特性,当识别出使用了尾递归时,会相应地只保留当前层函数的stack,节省内存,不会发生stackOverflowException调用栈溢出。

注意:
JVM缺乏尾调用指令,java编译器对尾递归的优化未实现,所以在java中尽量避免过深的递归调用,如果需要使用递归,建议优化成迭代式。
*

function story() {    
     从前有座山,
     山上有座庙,
     庙里有个老和尚,
     一天老和尚对小和尚讲故事:
     story() // 尾递归,进入下一个函数不再需要上一个函数的环境了,得出结果以后直接返回。
}


/**
* 尾递归的精髓在于往内进行下一层解析的时候,这一层函数执行完了,不再需要在保留出栈时需要从当前
的执行到的行、相关变量值继续的信息。所以空间复杂度是O(1)。
*/
function story() {    
      从前有座山,
      山上有座庙,
      庙里有个老和尚,
      一天老和尚对小和尚讲故事:
      story(),
      小和尚听了,
      找了块豆腐撞死了 // 非尾递归,下一个函数结束以后此函数还有后续,
                      //所以必须保存本身的环境以供处理返回值。
}

尾递归就是操作的最后一步是调用自身的递归。注意,尾递归的判断标准是函数运行 最后一步 是否调用自身,而不是是否在函数的最后一行调用自身。
这个不是尾递归:

function fibonacci(n) {
  ...
  if (n === 1) return 1;
  ...
  return n * fibonacci(n-1); // 最后一步是乘法运算,而不是调用自身,因此不是尾递归
}

这个是尾递归:

function fibonacci(n,m,result) {
  ...
  if (n === 1) return 1;
  ...
  return fibonacci(n,n-1,n*(n-1));
}

3.二叉树遍历中的两次递归示例

/**
     *                         10
     *                      /      \
     *                    6         14
     *                   /  \       / \
     *                  5    8     12  19
     *                 /      \          \
     *                4        9          11
     *
     */
    public void preOrderVisit(Node n){


        //退出递归的条件:到达叶子节点
        if(n == null) return;

        System.out.println("<前序>--左/右子树圧栈--当前节点值 ---> " + n.value);


        /**递归,实际是圧栈过程,可以理解为分两个过程执行:
        step1:递归嵌套过程(圧栈过程,圧栈时记录当前函数执行状态,包括当前执行  
          到哪行、当前局部变量值等,传递给被调用函数(自己)),即调用自身  
          的过程,圧栈时在递归调用前的每一行代码都被执行,递归调用后面的代  
          码不能到达执行(递归处可理解为while(true)循环)。一直到达左侧遍
          历递归终止条件。


        类似while true循环,连续输出10 6 5 4
        */

        preOrderVisit(n.left);  //4:无下一个左子节点,执行下一行命令
        //此处左子树函数站开始出栈,4已出栈,从5开始回溯到根节点,回溯过程执行的是递归命令后面的代码行
        System.out.println("<中序>--左子树出栈--当前节点值--->" + n.value);//左子树结束递归嵌套后执行该行
                                //当前是节点4,输出中序4

        preOrderVisit(n.right);//转入执行该递归嵌套。后续左子树每一次出栈都进入右子树递归,转进右子树的入栈、出栈过程,4无右子树,未进入递归圧栈过程,执行下一行

        System.out.println("<后序>--右子树出栈--当前节点值--->" + n.value);//输出后续4


    }

参考:
[1].什么是尾递归
[2].尾调用优化
[3].栈是如何实现递归的:递归与栈一些不得不说的事
[4].stack 的三种含义

相关文章

  • 递归、调用栈及尾递归

    1.递归与栈 无论是否递归调用,当在一个函数(外层函数)的运行期间调用另一个函数(被调用函数,即内层函数)时,在运...

  • 9. 递归函数

    使用递归函数需要注意防止栈溢出解决递归调用栈溢出的方法是通过尾递归优化遗憾的是,大多数编程语言没有针对尾递归做优化...

  • 尾递归优化

    “尾递归优化”的含义是:如果递归函数属于尾递归,那么运行时会优化其调用过程。优化主要针对调用栈,将多层调用,转化为...

  • 递归&尾递归

    调用栈的特点,先进后出, FILO, 场景还原。 递归 有栈溢出的可能 stack overflow 尾递归 编译...

  • 尾递归

    尾递归 Lua尾递归的实现 爆栈问题 基于栈实现函数调用的语言都有栈空间的上限,这里拿几个语言举例 运行到2589...

  • python学习

    1:尾递归 解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊...

  • 递归(recursion)

    基线条件(base case)&递归条件(recursive case) 递归条件基线条件 堆栈 调用栈 递归调用栈

  • 算法图解系列之递归[03]

    3 递归 3.1 递归<函数> 3.2 基线条件和递归条件 3.3 递归调用栈

  • 什么是尾调用?什么是尾递归?尾调用的优化?尾递归优化?

    尾调用优化 尾递归(尾调用优化)

  • 递归调用优化

    尾递归优化 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。 递归非常耗费内存,因为需要同时保存成千上百个调...

网友评论

      本文标题:递归、调用栈及尾递归

      本文链接:https://www.haomeiwen.com/subject/icymaqtx.html