递归算法的非递归实现

栈的作用

当前问题执行到一个状态,以现有的条件无法完全解决时,必须先记下当前状态,然后继续往下执行,等条件成熟后再返回解决。
如DFS时,当前节点1,沿着邻接点2往下遍历,后面还要回到节点1继续遍历其他邻接点。

背景

最近做题遇到过几次递归实现的算法,要求你用非递归的方式实现。这里做一个总结。其实也没技巧,再看几遍,多默写几次代码,记熟即可。
递归算法基本都涉及到函数调用栈。每次递归调用时都将函数数据入栈,所以递归调用越深,占用的栈空间越多。如果层数过深,肯定会导致栈溢出,这也是消除递归的必要性之一。

1. 直接转换法

用循环结构代替单向递归和尾递归。

单向递归:简单的说是指递归的过程总是朝着一个方向进行。函数1调用函数2再调用函数3…一只不重复调用之前的函数。
尾递归函数是以递归调用结尾的函数,是单向递归的特例。

尾递归的递归调用语句只有一个,而且是放在过程的最后。当递归调用结束并返回时,上层函数也就结束了。无需关注函数地址、参数、局部变量等,因此可以直接采用循环写出非递归过程。

斐波那契数列的递归

int Fib(int n) {
  if (n<=1) 
  	return n;
  else
  	return Fib(n - 1) + Fib(n - 2);//尾递归调用
}

斐波那契数列的非递归

int Fib(int n) {
  if (n<=1) 
  	return n;
  else//循环实现
  	int x1= 1;
 	int x2= 0;
 	int sum;
 	for(int i = 2;i < = n; i++) {
    	sum= x1+x2;
  	x2= x1;
    	x1= sum;
   }
  	return sum;
}

如何由递归转循环呢,我的思路是列出递归每次的值,然后找规律:

0 1 1

nx1x2SUM
0AB0
1CD1
2101
3112
4213
5325
6538
0、1的时候直接是n,因此循环从2开始,x1初始为0,x2初始为1;依次递增到n(n>=2)结束,执行n-1次
	int x1= 1;
 	int x2= 0;
 	int sum;
	for(int i=2;i<=n;++i)

再观测,x2等于上一次相加的x1,x1等于上一次相加的sum。则循环体内为:

	sum= x1+x2;
	x2= x1;
	x1= sum;

直接用循环替代递归不是很难,一般找清楚递归的变化规律就可以做出。

2. 间接转换法

用于需要回溯的递归;如:函数1调用函数2,函数2又要回溯到函数1;再由函数1调用函数3;(想象树的遍历)我们可以根据函数调用栈,手动建立一个栈,实现非递归实现。

int Stack[Maxsize],top=-1
visit(a);
Stack(++top)=a//将初始状态a进栈
while(top!=-1)//栈不为空
{
	a1=Stack(top);//将栈顶元素赋给a1
	//从a1中寻找满足条件的a2
	if(a2 true)//找到了
	{
		visit(a2);//访问a2;
		Stack(++top)=a2;//将a2进栈
	}
	else//未找到,a1遍历完毕
		---top;//a1退栈
}

注意一点就是:先读取栈顶元素,找到栈顶元素满足条件的下一个元素入栈。因为此时该元素没有出栈+循环的缘故,上层栈内元素出栈后,会回溯到该元素,直到该元素所有满足条件的元素都被访问,该元素出栈,就不会被回溯。这就模拟了函数的递归调用栈。
在数据结构中递归间接转换非递归应该有很多,不够我目前遇到的主要还是二叉树遍历和图的深度遍历,下面做一个记录:

二叉树先序遍历

void preorder(BTNode *p)
{
	if(p!=NULL)
	{
		visit(p);
		preorder(p->lchild);
		preorder(p->rchild);
	}
}

先序遍历
观察先序遍历的入栈过程:
根节点入栈并访问
根节点左子树各节点入栈并访问,各节点出栈
根节点右子树各节点入栈并访问,各节点出栈
根节点出栈

void preorder(BTNode *bt)
{
	if(bt!=NULL)
	{
		BTNode *STack[Maxsize];
		int top=-1;
		BTNode *p;
		Stack[++top]=bt;
		while(top!=-1)
		{
			p=Stack[top--];
			visit(p);
			if(p->rchild!=NULL)
				Stack[++top]=p->rchild;
			if(p->;child!=NULL)
				Stack[++top]=p->rchild;
		}
	}
}

模拟先序遍历:
根节点入栈
循环
-----栈顶元素出栈,访问;
-----栈顶元素右孩子入栈;左孩子入栈;
直到栈空

注意:真实递归的话是左子树先入栈,依次遍历并出栈后右子树才入栈。
我们这边模拟为了简化,是右孩子先入左孩子后入。以此实现先访问左孩子后访问右孩子。

中序:

while(top!=-1||p!=NULL)//存在节点出栈后栈空(根节点),但是p非空,右子树还可以入栈。
{
	while(p!=NULL)//一直向左入栈,直到节点没有左孩子
	{
		Stack[++top]=p;
		p=p->lchild;
	}
	if(top!==-1)//节点出栈,访问,右孩子
	{
		p=Stack[top--];
		visit[p];
		p=p->rchild;
	}
}

节点入栈时,节点左孩子入栈;
直到节点左孩子不存在,节点出栈,节点右孩子入栈;
直到栈空

后序:

while(top!=-1)//按根右左入栈1
{
	p=Stack1[top1--];
	Stack2[++top2]=p;//每次栈顶元素出栈都入栈2
	if(p->lchild!=NULL)
		Stack1[++top1]=p->lchild;
	if(p->rchild!=NULL)
		Stack1[++top1]=p->rchild;
}
whild(top2!=-1)//栈2就是根右左的逆序————>左右根
{
	p=Stack2[top2--];
	visit(p);
}

栈1中根右左顺序入栈2
栈2中即可实现逆序,左右根

深度遍历

DFS(G,V){
	......
	visit(v)
	visited[v]=1;
	for(w=g->adjlist[v1].firstarc;w!=NULL;w=w->nextarc)
	{
		if(visited[w]!=1)
		{
			DFS(g,w->adjvex);
		}
	}
}

访问v0;v0此时已在函数栈中
找v0的第一个为未访问的边;
找到就递归调用边指向的邻接点v1

访问邻接点v1;邻接点v1此时已在函数栈中
找v1的第一个为未访问的边;
找到就递归调用边指向的邻接点v2
没有找到就结束递归,v2出栈

回溯访问v0;v0此时还在函数栈中
找v0的第一个为未访问的边;(此时指向v1的边已经访问了)
找到就递归调用边指向的邻接点v3

int Stack[Maxsize],top=-1
visit(v);//访问第一个结点
visited[v]=1;
Stack(++top)=v//将初始结点进栈
while(top!=-1)//栈不为空
{
	v1=Stack(top);//将栈顶元素赋给v1
	p=g->adjlist[v1].firstarc;//该结点的第一条边
	while(p!=NULL&&visited[p->adjvwx]=1)//遍历该结点的adjlist,找到第一个未访问的邻接点
		p=p->nextarc;
	if(p==NULL)//遍历结束,该结点的所有邻接点都访问了
		---top;//该结点退栈
	else//找到邻接点
	{
		visit(p->adjvex);//访问邻接点
		visited[p->adjvex]=1;
		Stack(++top)=p->adjvex;//将邻接点入栈
	}
}

访问v0并入栈;
提取栈顶v0;找v0的第一个未访问的边;
找到了;访问边指向的邻接点v1并入栈
没找到;V0出栈

参考

[1] https://blog.csdn.net/fbz123456/article/details/50959412.
新开通了本人的公众号,欢迎关注:燕南路GISer ,专注GIS干货分享,不定期更新。
主要兴趣:GIS、时空数据挖掘、python、机器学习深度学习
提问、求资源等都可在公众号后台留言
CSDN的部分内容会重写再搬迁到公众号,欢迎关注!
在这里插入图片描述

  • 5
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

燕南路GISer

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值