参考文章:How to replace recursive functions using stack and while-loop to avoid the stack-overflow.
链接:How to replace recursive functions using stack and while-loop to avoid the stack-overflow - CodeProject
论文是外国文献,全英文版的。讲述了用栈消除递归的一些方法,蒻姬读了之后写篇文总结了一下~~
首先,为何要用堆栈消除递归?一方面来说,对于数据量比较大的输入,可以防止溢出;另一方面,这种方式更接近本质,我们可能会用递归轻松的写出一道题,但是我认为真正能用非递归写的话,也是一种能力。
当然,文章最后也提到了,递归函数是一个条理非常清晰,简洁明了的写法。所以在写代码时候不妨先写出递归,必要的时候再消除递归。
言归正传,用堆栈消除递归,本质上来说就是个模拟过程。文章对这个过程写得很清楚了,下面我再总结一下十条规则:
1.创建一个结构体,包含三个变量:n//输入 test//局部变量 stage//状态量(执行阶段)
2.创建返回值变量,并且初始化返回值,表示返回函数在递归中的作用。(void类型可以忽略)
3.创建栈。
4.初始化数据,并且压入栈。
5.创建循环,条件为栈非空,每一次循环执行完都将被执行的结构体弹出栈。
6.这一点很重要,stage分为调用阶段和调用返回后执行的阶段,因为两个阶段所执行的程序不同。如果有两次调用,需要用三个状态。总之,根据递归调用,可以分为不同状态,毕竟调用就是一个新的代码块。
7.根据stages执行不同状态(废话。。。)
8.如果递归函数有返回值,需要进行存储。
9.返回关键词在非递归函数中写作continue(视情况而定)
10.最后一点也是整个算法的重头戏。步骤是:如果是执行的递归函数,首先改变结构体的状态,将其压入栈中。然后创建新的结构体并初始化数据,压入栈中。
我以我的理解解释一下:在递归函数中,如果再次申请调用,本次调用并没有执行完,当被调函数执行完后才会执行后面的语句。这时在非递归中,可以理解为状态改变了,这就是改变状态还要压入栈的原因:后面的状态以后还会执行。
模板代码:
int SomeFunc(int n, int &retIdx) //递归函数
{
...
if(n>0)
{
int test = SomeFunc(n-1, retIdx);
test--;
...
return test;
}
...
return 0;
}
C++
// Conversion to Iterative Function
int SomeFuncLoop(int n, int &retIdx)
{
// (First rule)
struct SnapShotStruct {
int n; // - parameter input 输入
int test; // - local variable that will be used 函数内局部变量
// after returning from the function call
// - retIdx can be ignored since it is a reference.
int stage; // - Since there is process needed to be done
// after recursive call. (Sixth rule)执行阶段
};
// (Second rule)
int retVal = 0; // initialize with default returning value 返回值变量
// (Third rule)
stack snapshotStack;
// (Fourth rule)
SnapShotStruct currentSnapshot; //初始化
currentSnapshot.n= n; // set the value as parameter value
currentSnapshot.test=0; // set the value as default value
currentSnapshot.stage=0; // set the value as initial stage
snapshotStack.push(currentSnapshot);
// (Fifth rule)
while(!snapshotStack.empty())
{
currentSnapshot=snapshotStack.top();
snapshotStack.pop();
// (Sixth rule)
switch( currentSnapshot.stage)
{
case 0: //调用函数阶段
// (Seventh rule)
if( currentSnapshot.n>0 )
{
// (Tenth rule)
currentSnapshot.stage = 1; // - current snapshot need to process after 改变状态
// returning from the recursive call
snapshotStack.push(currentSnapshot); // - this MUST pushed into stack before 这次调用还没执行完
// new snapshot!
// Create a new snapshot for calling itself
SnapShotStruct newSnapshot;
newSnapshot.n= currentSnapshot.n-1; // - give parameter as parameter given
// when calling itself
// ( SomeFunc(n-1, retIdx) )
newSnapshot.test=0; // - set the value as initial value
newSnapshot.stage=0; // - since it will start from the
// beginning of the function,
// give the initial stage
snapshotStack.push(newSnapshot); //下次调用压入栈
continue;
}
...
// (Eighth rule)
retVal = 0 ;
// (Ninth rule)
continue;
break;
case 1:
// (Seventh rule)
currentSnapshot.test = retVal;
currentSnapshot.test--;
...
// (Eighth rule)
retVal = currentSnapshot.test;
// (Ninth rule)
continue;
break;
}
}
// (Second rule)
return retVal;
}
以上即为文章所给出的模拟递归函数的通法。但是并不意味着每个递归函数转化为非递归时都需要这样模拟。其实大部分情况下,我们都可以用递推来实现。比如求阶乘,斐波那契数列这样的,以及大多数动态规划问题,可以写出递推式的,可以直接将递归转化为递推式,用循环递推即可,不需要用堆栈。
按照上面的思路,下面我用非递归写汉诺塔问题:
递归函数形式:
#include
using namespace std;
void move(int n,char x,char y,char z)
{
if(n==1) cout<"<"<
测试运行结果:
非递归形式:
思路:比较简单的一类模拟,首先,函数没有返回值,不需要返回值变量。另外,递归函数有两次调用,所以有三个阶段。
#include
#include
using namespace std;
struct node{
int n;
char x;
char y;
char z;
int stage; //状态变量
};
void move(int n,char x,char y,char z)
{
node currentnode;
currentnode.n=n;
currentnode.x=x;
currentnode.y=y;
currentnode.z=z;
currentnode.stage=0; //第一次调用初始化
stackS;
S.push(currentnode);
while(!S.empty()){
currentnode=S.top();
S.pop();
switch(currentnode.stage){ //判断状态
case 0: //执行move(n-1,x,z,y);
if(currentnode.n==1) cout<"<"<"<
测试运行结果:
读者有兴趣可以试试用非递归写快排和归排,当然不一定用上述方法。