递归与非递归的转换

本文主要讨论将递归程序转化为非递归程序的一般化方法。主要参考了递归转化为非递归的一般方法一文。

一个普通的递归程序,使用栈来保存局部变量和返回地址。此处提及的栈:栈区(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;
	}

常见的迷宫求解问题,可以理解为一个具有限制条件的四叉树遍历问题,其本身具有递归性质,也可以使用模拟递归的方法方便地给出非递归求解。更一般地,模拟递归的方法实际上可以作为一个模板,适用于其他递归程序的改写。


  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值