使用栈来模拟递归过程
一.为什么要学习递归与非递归的转换的实现方法?
1)并不是每一门语言都支持递归的.
2)有助于理解递归的本质.
3)有助于理解栈,树等数据结构.
二.预先知识
1)《数据结构》栈,二叉树,树的相关知识
2)对递归有明确的认识
三.图的深度优先搜索
最近写有关图的算法,首先写的便是基础中的基础,深度优先与广度优先搜索。写完这两个算法的时候,看到了算法导论课后一题,讲的是将深度优先算法使用栈来模拟。
先贴出写下的有关深度优先搜索的代码
void DFS(MatrixPtr mptr, int d[], int f[],int father[], int color[])
//其中d[]用于记录结点第一次被发现时的递归的深度,f[]用于记录完成深度优先探索时的深度
{
int temp = mptr->column,time=0;
for(int i=0; i!=temp; i++)
{
color[i] = WHITE;
d[i] = 0;
f[i] = 0;
father[i] = NIT;
}
for(int i=0; i!=temp; i++)
{
time=0;
if(color[i]==WHITE)
DFS_VISIT(i,mptr,&time,d,f,father,color);
}
}
void DFS_VISIT(int i,MatrixPtr mptr, int* time, int d[], int f[], int father[],int color[])
//i表示参与深度优先搜索的标号,返回值表示当前的深度
{
d[i] = (*time)++;
color[i] = GREY;
for(int j=0; j!=mptr->column; j++)
if(mptr->matrix[i][j]&&color[j]==WHITE)
{
father[j] = i;
DFS_VISIT(j,mptr,time,d,f,father,color);
}
color[i] = BLACK;
f[i] = ++*time;
}
其实两个函数里只有DFS_VISIT中存在递归程序。于是开始写,最初写程序,其实写的是非常的混乱的。后来,经过网上开了相应的文章才有所顿悟。尤其是这篇
http://www.chinaunix.net/old_jh/23/331522.html如何用栈实现递归与非递归的转换
带给我非常大的触动。文中提出 “递归与非递归的转换基于以下的原理:所有的递归程序都可以用树结构表示出来.需要说明的是, 这个"原理"并没有经过严格的数学证明. 有三种方法可以遍历树:前序,中序,后序.理解这三种遍历方式的递归和非 递归的表达方式是能够正确实现转换的关键之处,所以我们先来谈谈这个.需要说明的是,这里以特殊的 二叉树来说明,不过大多数情况下二叉树已经够用,而且理解了二叉树的遍历,其它的树遍历方式就不难了. “
递归实际上是在执行到递归函数时,将现有的函数中现场保存入栈中,执行完函数后,恢复现场。于是在处理这类问题是,关键便是思考那些数据是必须要存入栈中的,这点非常的重要。(其实什么程序,预先思考都是非常重要的)
我开始观察这段代码,发现DFS_VSIT只是树的后序遍历。同时在执行递归是,必须装入当前的i值与执行到for循环中的哪一步。然后还要装入下一次要完成的i值。
现在给出完成的代码
void DFS_VISIT2(int i,MatrixPtr mptr, int time, int d[], int f[], int father[],int color[])
//i表示参与深度优先搜索的标号,返回值表示当前的深度
{
stack stk;
stk.push(i);//推入有节点,几乎所有类似的题目都是这么写的
stk.push(-1);
while(!stk.empty())
{
int j = stk.top()+1;//由于推入的j还原的之后必然要加1,所以这么处理
stk.pop();
int i = stk.top();
stk.pop();
if(j==0)
{
d[i] = time++;
color[i] = GREY;
}
for(; j!=mptr->column; j++)
if(mptr->matrix[i][j]&&color[j]==WHITE)
{
father[j] = i;
stk.push(i);
stk.push(j);
stk.push(j);
stk.push(-1); //最初是是使用一个if语句来判断是否是返回上一个的递归//状态中,后来想到干脆推入新的新的结点是也推入一个初始的j值
break;
}
if(j==mptr->column)
{
color[i] = BLACK;
f[i] = ++time;
}
}
}
四.二叉树的遍历算法
为了对上述方法,进行验证,我对二叉树的三种遍历进行了尝试。
typedef struct BitNode
{
unsigned int data;
BitNode* lchild,* rchild;
}BitNode,* BitNodePtr,** BitNodePtrPtr;
先序遍历比较的简单只存在访问数据,两个子树指针的入栈
void PreTravel2(BitNodePtr bitptr)//使用栈替代递归的算法
{
stack stk;
stk.push(bitptr);
while(!stk.empty())
{
BitNode* tempbitptr = stk.top();
stk.pop();
if(tempbitptr!=NULL)
{
visit(tempnbitptr);
stk.push(tempbitptr->rchild);
stk.push(tempbitptr->lchild);
}
}
}
中序遍历最初的想法是,先将左子树的指针一口气推到底,在访问数据,然后在推入右子树。
void InTravel3(BitNodePtr bitptr)
{
stack stk;
stk.push(bitptr);
while(!stk.empty())
{
BitNodePtr p = stk.top();
while (p !=NULL ) /* 向左走到尽头 */
{
stk.push(p->lchild);
p=stk.top();
}
stk.pop(); /* 空指针退栈 */
if (!stk.empty())
{
p = stk.top();
stk.pop();
visit(p); /* 访问当前结点 */
stk.push(p->rchild); /* 向右走一步 */
}
}
}
在这里巧妙地利用了空指针与退栈操作。
不过我在写这段代码的,时候出现了一些问题那就是无法有效的判断是否已经完成了左子树的访问。为此依据他人的写法完成后,在我又另外写了一段代码,来弥补上次的失误。
void InTravel2(BitNodePtr bitptr)
{
stack stk;
Node d = {bitptr,true};
stk.push(d);
while(!stk.empty())
{
BitNodePtr temptr = stk.top().p;
Nodeptr top = &stk.top();
while(stk.top().flag&&temptr!=NULL&&temptr->lchild!=NULL)//推入所有的左子树内容
{
temptr = temptr->lchild;
Node d = {temptr,true};
stk.push(d);
}
top->flag = false;//关键的地方
temptr = stk.top().p;
stk.pop();
if(temptr!=NULL)
{
Visit(temptr);
Node d = {temptr->rchild,true};
stk.push(d);
}
}
}
在标有注释的地方将已经推入所有的左子树的指针的标志记为false,下次在访问的时候就不会在进行推左子树的操作。
为什么这么做呢,因为在思考中序遍历的操作是发现恢复现场的时候,实际上是恢复到了推完所有的左子树的瞬间。因此在这里写的时候,理由一个flag来判断是否处于需要左子树的状态。
后序遍历
与前面的中序遍历相似,在这里有一个flag,来判断是否处于需要推入子树的阶段。
在这里解释一下,在后序遍历的时候,先访问左右的子树,再访问自身,恢复现场时,是已经访问完左右子树的状态。同时,如果是叶节点的话直接访问。
void AfterTravel1(BitNodePtr bitptr)
{
stack stk;
Node d = {bitptr,false};
stk.push(d);
while(!stk.empty())
{
Node d = stk.top();
stk.pop();
if(d.flag||NULL==d.p->lchild&&NULL==d.p->rchild)
{
visit(d.p);
}
else
{
Node e = {d.p,true};
stk.push(e);
if(d.p->rchild!=NULL)
{
Node e = {d.p->rchild,false};
stk.push(e);
}
if(d.p->lchild!=NULL)
{
Node e = {d.p->lchild,false};
stk.push(e);
}
}
}
}
五.最后的总结
多思考,多尝试。在写之前,不妨在纸上模拟一下状态。