一、前言
- 最近在刷树、图相关的题,很多题都用到了递归做法,但是其实这些题也有非递归做法,而我也经常用非递归解法。
- 这篇博客的目的就是写写我对递归和非递归的看法。(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的值,小于的话继续"递归",大于等于的话,当前栈桢结束,出栈,继续执行栈顶的栈桢。
- 当然,具体情况要还是要具体分析。