递归与非递归

一、前言

  • 最近在刷树、图相关的题,很多题都用到了递归做法,但是其实这些题也有非递归做法,而我也经常用非递归解法。
  • 这篇博客的目的就是写写我对递归和非递归的看法。(java中)



二、递归

  • 对于递归,其实是一种隐式调用栈的过程,看如下代码:
public class RecursionTest {

    public static void main(String[] args) {
        System.out.println(factorial(6));
    }

    private static int factorial(int n) {
        if (n == 0 || n == 1) {
            return n;
        }
        return factorial(n - 1) * n;
    }

}

  • 以上代码是一个简单的递归,结果是8
  • 通过debug,在递归边界条件中打断点可以看到:

在这里插入图片描述

  • 此时蓝色图标的是此时当前线程的虚拟机栈栈顶的栈桢,左边的Variables是变量,意思是这个栈桢保存的局部变量。
  • 对于下图:

在这里插入图片描述

  • 这是栈底的栈桢,也是第一个入栈的元素,局部变量表保存着这个栈桢的变量,当在它前面的栈桢执行结束并返回值轮到它执行的时候,它能够恢复原来的状态,因此,我们可以可以通过模拟一个栈,来模拟递归。



三、非递归

  • 代码:
public class NotRecursionTest {

    public static void main(String[] args) {
        System.out.println(factorial(6));
        System.out.println(factorial(5));
    }

    private static int factorial(int n) {
        int top = -1;
        // 对于要模拟的递归, 要保存并恢复的局部变量就一个, 因此可以很简单的模拟一个栈(java递归中方法
        // 间的调用要是太多了, 比如出现无限递归(不会弹栈只会压栈)的情况, 会报栈溢出)
        int[] stack = new int[n];
        while (n > 0) {
            // 此时压栈, 相当于保存每个方法的局部变量
            stack[++top] = n--;
        }
        // 压栈结束, 并开始从栈顶元素回归到栈底元素并得到返回值
        int result = stack[top--];
        while (top != -1) {
            // 出栈, 并且将出栈的"栈桢中的变量n"与上一次的结果做运算, 得到的结果"返回"给栈顶元素的"栈桢"
            result = stack[top--] * result;
        }
        // 最后栈空, 相当于所有方法执行完, 结束并返回结果
        return result;
    }

}

  • 看代码的解释,同时也对以上代码的"递归边界"打断点debug。

在这里插入图片描述

  • 可以看到,模拟的栈保存了还在"递归路上的方法的状态",以便恢复。

  • 运行结果:

720
120
  • 甚至可以模拟地更加像jvm的栈,看如下代码:
public class Casual {

    public static void main(String[] args) {
        System.out.println(factorial(6));
        System.out.println(factorial(5));
    }

    private static int factorial(int n) {
        int top = -1;
        StackFrame[] stack = new StackFrame[n];
        while (n > 0) {
            // 此时压栈, 相当于保存每个方法的局部变量
            stack[++top] = new StackFrame(n--);
        }
        // 压栈结束, 并开始从栈顶元素回归到栈底元素并得到返回值
        StackFrame cur = stack[top--];
        // 对于栈桢内的方法, 用操作数栈辅助运算
        // 这一步是将局部变量表位置1的值返回, 因为这里是边界
        int result = cur.localVariableTable[0];
        while (top != -1) {
            // 出栈, 并且将出栈的"栈桢中的变量n"与上一次的结果做运算, 得到的结果"返回"给栈顶元素的"栈桢"
            cur = stack[top--];
            cur.operandsStack[++cur.top] = cur.localVariableTable[0];
            // 操作数弹栈并和上一次返回的结果做运算
            result = result * cur.operandsStack[cur.top--];
        }
        // 最后栈空, 相当于所有方法执行完, 结束并返回结果
        return result;
    }

}

class StackFrame {

    /**
     * 局部变量表: 其实局部变量表要存放的数据很多, 比如数据的类型和数据的值, 我这里只是模拟
     * 局部变量表的大小会在编译时根据代码确定好
     * 这里也假装编译时确定好长度了
     */
    int[] localVariableTable = new int[1];

    /**
     * 操作数栈: 这里是模拟
     * 操作数栈的大小会在编译时根据代码确定好
     * 我这里也假装编译时确定好长度了
     */
    int[] operandsStack = new int[2];
    int top = -1;

    public StackFrame(int localVariable0) {
        // 虽然局部变量表早就确定好长度, 但是它的值是在运行时赋值的
        this.localVariableTable[0] = localVariable0;
    }

}
  • 上面的代码就模拟了一下jvm的虚拟机栈里的方法递归:
  • 对"递归边界"打断点:

在这里插入图片描述

  • 可以看到,每个"栈桢"都保存了自己的状态,当出栈的时候可以还原自己的状态。
  • 当然,实际的jvm虚拟机栈是复杂很多的,比如局部变量表不仅保存局部变量,还保存了局部变量的类型的,局部变量表对于long和double是分配两个slot位置的,其它的比如int、short和地址引用类型等都是分配一个slot。而且jvm虚拟机栈中每个栈桢在调用其它方法的时候(当前栈桢上面再入栈新的栈桢),会保持自己的状态的;当调用方法结束的时候,要从调用方法的位置继续往下执行直到结束,然后这个栈桢出栈,栈顶的栈桢恢复原来执行到的位置继续往下执行…直到虚拟机栈栈空,结束一个线程。
  • 这里扯远了,但是我认为如果用栈去做非递归,模拟递归的话,状态的保存很重要。



四、总结

  • 对于用栈模拟递归做非递归,我认为在入栈前保存状态是很重要的,如果是要保存的状态只有一个,那么完全可以用一个数组模拟栈来做。
    • 对于不确定栈要多大的情况下,可以写一个栈类并作扩容;
    • java的栈感觉做的不太好,连初始容量都不能给,因此遇到可以得知栈的深度的情况,我不会用java的栈,因为很大概率会有扩容,导致时间复杂度更大。
  • 如果遇到像斐波那契那种递归,一个递归里面两个递归,对于jvm的虚拟机栈来讲,因为方法是跟着字节码执行的,jvm执行完第一个递归,会来到第一个递归结束后(恢复),接着去执行第二个递归;对于用栈来做非递归来讲,是很难做到类似jvm那样的,因此我们可以偷偷在栈桢类中加个计数器,记录执行了几次递归,当执行了次数等于多少时才出栈。
    • 比如图的深度遍历,需要对当前结点的所有邻接结点进行深度遍历,也就是递归,可以在每一个"栈桢对象"中的局部变量表加一个槽位slot0来记录邻接结点数量的变量,也就是每一个栈桢中的结点要遍历的邻接结点,再在局部变量表中加一个槽位slot1来记录当前遍历了多少个邻接结点,遍历结束一个邻接结点后(此时栈顶就是当前栈桢),slot1的值+1,然后判断是否小于槽位slot0的值,小于的话继续"递归",大于等于的话,当前栈桢结束,出栈,继续执行栈顶的栈桢。
  • 当然,具体情况要还是要具体分析。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
斐波那契数列可以通过递归算法和非递归算法来实现。递归算法是通过定义递归函数,以前两个数的和作为当前数的值。非递归算法是通过迭代的方式,使用三个变量来保存前两个数的值,并不断更新这三个变量的值来计算当前数的值。 递归算法的实现如下: ``` long long Fib(int N){ if (N < 3) return 1; return Fib(N - 1) + Fib(N - 2); } ``` 这个算法的基本思想是,当N小于3时,直接返回1;当N大于等于3时,返回前两个数的和。但是需要注意的是,递归算法的效率较低,因为有很多重复的计算。 非递归算法的实现如下: ``` long long Fib(int N){ int First = 1; int Second = 1; int Third = 1; while (N > 2){ Third = First + Second; First = Second; Second = Third; N--; } return Third; } ``` 这个算法使用三个变量来保存前两个数的值,然后不断更新这三个变量的值来计算当前数的值。当N大于2时,通过循环不断更新变量的值,直到计算到第N个数为止。 需要注意的是,斐波那契数列在用递归算法实现时,可能会遇到溢出的问题。因此,在使用递归算法实现时,需要注意控制递归的深度,以避免出现溢出的情况发生。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [斐波那契数列(递归非递归算法)](https://blog.csdn.net/qq_45328505/article/details/103027461)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [斐波那契数列的递归算法与非递归算法](https://blog.csdn.net/qq_33951180/article/details/52484080)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [JAVA递归非递归实现斐波那契数列](https://download.csdn.net/download/weixin_38519234/12763817)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值