目录)
- 栈的作用
- 背景
- 1. 直接转换法
- 2. 间接转换法
- 二叉树先序遍历
- 深度遍历
- 参考
栈的作用
当前问题执行到一个状态,以现有的条件无法完全解决时,必须先记下当前状态,然后继续往下执行,等条件成熟后再返回解决。
如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
n | x1 | x2 | SUM |
0 | A | B | 0 |
1 | C | D | 1 |
2 | 1 | 0 | 1 |
3 | 1 | 1 | 2 |
4 | 2 | 1 | 3 |
5 | 3 | 2 | 5 |
6 | 5 | 3 | 8 |
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出栈