本文主要讨论将递归程序转化为非递归程序的一般化方法。主要参考了递归转化为非递归的一般方法一文。
一个普通的递归程序,使用栈来保存局部变量和返回地址。此处提及的栈:栈区(stack)由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
这一点具体现在编译后的汇编代码上,我们来看一个例子,编译如下c++代码。
#include <stdio.h>
void fun(int depth){
if (depth == 0){
return;
}
fun(depth-1);
}
void main(){
fun(10);
}
编译后生成的fun()汇编代码,可以看到在函数开始和结尾分别使用了push和pop对变量和返回地址进行了压栈与弹出。
?fun@@YAXH@Z PROC ; fun, COMDAT
; 3 : void fun(int depth){
push ebp
mov ebp, esp
sub esp, 192 ; 000000c0H
push ebx
push esi
push edi
lea edi, DWORD PTR [ebp-192]
mov ecx, 48 ; 00000030H
mov eax, -858993460 ; ccccccccH
rep stosd
; 4 : if (depth == 0){
cmp DWORD PTR _depth$[ebp], 0
jne SHORT $LN1@fun
; 5 : return;
jmp SHORT $LN2@fun
$LN1@fun:
; 6 : }
; 7 : fun(depth-1);
mov eax, DWORD PTR _depth$[ebp]
sub eax, 1
push eax
call ?fun@@YAXH@Z ; fun
add esp, 4
$LN2@fun:
; 8 : }
pop edi
pop esi
pop ebx
add esp, 192 ; 000000c0H
cmp ebp, esp
call __RTC_CheckEsp
mov esp, ebp
pop ebp
ret 0
?fun@@YAXH@Z ENDP ; fun
为了能够将递归代码改写成非递归代码,我们需要手动模拟一个压栈与弹出的过程,下面使用二叉树遍历的例子进行解释。
二叉树中序遍历递归算法如下
void preorder(Node root){
if (root == null) {
return;
}
preorder(root.left);
visit(root);
preorder(root.right);
}
转化为非递归,我们首先定义一个Frame类,作为栈帧,栈帧中存储有必要的局部变量和返回地址,在这里next的作用类似于返回地址,它指代的是函数被压栈前应该执行的下一步操作。
static class Frame{
Node node;
int next;
}
接下来就是改写非递归算法。我们可以发现,在while循环中各个步骤的顺序与递归程序中是类似的,我们人为设置的多个if else可以视为将原先的递归程序进行分段,方便调用返回后选择接下来执行哪一段代码。
void template(node root){
LinkedList<Frame> stack = new LinkedList<>();
node cur = root; //node对应需要压栈的局部变量
int next = 0; // 实际上next代表程序返回后所要执行的下一步
while(!stack.isEmpty()){
if(cur == null){
Frame f = stack.pop();
cur = f.node;
next = f.next;
continue;
}else if(next == 0){
stack.push(new Frame(cur, 1); //压栈,进入对left节点的递归
cur = cur.left;
next = 0;
}else if(next == 1){
visit(cur); //为了得到不同的遍历顺序,只需要改变visit所处的位置即可
stack.push(new Frame(cur, 2)); //压栈,进入对right节点的递归
cur = cur.right;
next = 0;
}else if(next == 2){
Frame f = stack.pop(); //执行到函数末端,弹出并返回到上一层调用
cur = f.node;
next = f.next;
}
}
}
例如从root根节点开始,执行visit后,将当前root节点和“1”压栈,接下来访问左子树,当root的左子树被访问完成后,包含root的frame被弹出(相当于递归中内层调用完成,返回外层),程序将会执行next==1这一步,也就是访问root的右子树。
实际上对于非递归中序遍历,我们还有其他方法,但是模拟递归的方法更具一般性。
public List<Integer> inorderTraversal(TreeNode root) {
Stack<TreeNode> stack = new Stack<TreeNode>();
List<Integer> output = new ArrayList<Integer>();
do{
while (root != null) {
stack.push(root);
root = root.left;
}
if(stack.isEmpty())
return output;
TreeNode top = stack.pop();
output.add(top.val);
if (top.right != null) {
root = top.right;
stack.push(root);
root = root.left;
}
}while (!stack.isEmpty());
return output;
}
常见的迷宫求解问题,可以理解为一个具有限制条件的四叉树遍历问题,其本身具有递归性质,也可以使用模拟递归的方法方便地给出非递归求解。更一般地,模拟递归的方法实际上可以作为一个模板,适用于其他递归程序的改写。