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

递归、调用栈及尾递归

作者: 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 的三种含义

    相关文章

      网友评论

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

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