这里讲的消除递归是用栈来模拟系统的函数调用从而消除递归。

要说明一下的是,我说的栈就是Stack,后进先出的一种数据结构;而有的书翻译成堆栈。堆(heap)有两种意义。第一种是一种线性数据结构,满足node[i]>=node[2i+1],node[i]>=node[2i+2]。第二种一般是在程序设计语言动态申请内存的时候说的,代表一个系统内存区。内存的申请一般有两种。比如在一个函数中申请一个变量 int a,就是在栈中定义了一份空间。如果用Integer i=new Integer(0);,就是在堆中申请了一份内存空间。当函数结束,栈中的空间会自动释放(变量的作用域结束);而堆中的空间不会,C++的话要自己delete,否则就会造成内存泄露,而java会定时用垃圾回收器自动回收这些空间。这样看来堆(heap)和栈(stack)不但代表两种数据结构,而且代表两种内存使用方法。就栈来说,两者似乎比较统一——栈内存使用了栈这种数据结构;但堆内存和堆数据结构有什么关系呢?是不是系统堆使用了堆这种数据结构呢?我不知道。似乎管理内存用不了堆这种数据结构,堆数据结构主要用于实现优先队列。不知道内存管理是不是和优先队列有关?

由于消除递归是要使用goto语句,所以这部分的代码都是用c++实现的。

首先我们来看一个简单的函数调用。

int sum(int a,int b){
   return a+b;
}
  
    
void main(){
…..
int c=sum(3,4);
…….

我们在编写程序的时候会觉得函数调用太简单了。但如果是在汇编语言中,这并不是件容易的事,如果汇编语言中不让你使用CALL和RET指令怎么实现函数调用?

我们看看简单的一条int c=sum(3,4);语句,系统都为我们做了什么?

函数的调用分三个步骤:传递参数;调用函数;返回。

我们编写函数sum时,拿着a和b就使用。如果让我们自己传递参数,会怎么样呢?回忆一下在汇编语言中传递参数的方法。就是函数调用者和函数“约定”一个内存空间(或者寄存器)来存放参数。调用者先把参数放到约定好的地方,然后就准备调用函数,调用其实就是把函数的地址放到IP寄存器,当然为了能够调用后返回,还得把调用者调用完函数后的下一条指令的地址放到一个“约定”的位置,如果使用CALL和RET指令,这些工作它就帮你做了。还有一个要说明的是,在汇编语言中是没有返回值这个概念的。所谓的返回值其实就是对一个变量的修改。比如c=int(2,3);就是要根据输入参数2,3,修改c的值,在汇编语言中实现的方法是把c的地址作为一个参数放到一个约定的地点,sum函数把2+3的结果放到c的地址里。

下面看一个例子。这个函数模拟sum函数。stack用来传递参数,我们假设调用者和函数都能访问算stack。

首先,调用者把参数都压入stack。然后goto L_Sum;调用函数。

函数首先在预定的地点(stack中)取出参数,计算后修改了c。然后goto L0;返回。

 

 

void SimulatingCalling()
{
       int sum;
       int a=3;
       int b=4;
       Stack stack;
       stack.Push(a);
       stack.Push(b);
       stack.Push((int)&sum);
       cout<<"Calling Sum function"<<endl;
       goto L_Sum;
L0:
       cout<<"After Calling"<<endl;
       cout<<"Sum of "<<a<<" and "<<b<<" is "<<sum<<endl;
  
    
       goto L1;
L_Sum:
       cout<<"Entering Sum Function"<<endl;
       int *local_sum,addrofsum;
       stack.Pop(addrofsum);
       local_sum=(int*)addrofsum;
       int local_a,local_b;
       stack.Pop(local_b);
       stack.Pop(local_a);
       *local_sum=local_a+local_b;
       cout<<"Calling End"<<endl;
       goto L0;
  
    
//end of Sum function
  
    
L1:
       cout<<"end of SimulatingCalling()"<<endl;
}

 

当然,这里调用是在SimulatingCalling中的,而SimulatingCalling其实是一个函数管理者。它知道sum函数的地址在哪里,然后goto那里。如果我们不知道sum函数的地址呢?我们只想告诉它“我要调用sum函数,参数是3和4,&c,调用后转到某个地方”。它该怎么办呢?INVOKE指令就是用来干这个的。CALL是没有参数的,你不能            CALL (3,4,&c),传递3,4,&c的任务是由调用者完成的,就想代码中的调用前得自己把他们压入stack。而INVOKE就把这些工作做好了,你如果使用INVOKE 3,4,&c,它就会把3,4,&c压入。

3,4,&c的压入方式可以是先3,后4,再&c,也可以是先&c,后4,再3。这就有调用方式的区别。从右到左的是STDCALL和C调用习惯;而从左到右是PASCAL调用习惯。

STDCALL和C的区别是清除堆栈的任务不同。前者是把这个任务交给了函数,后者则叫给了调用者。什么是清除堆栈?比如我们把3,4,&c压入了堆栈,这时ESP会指向了不同的位置,调用结束后应该恢复。如果是STDCALL,那么函数一个承担这个责任,结束前RET 12 ;C的话就要调用者在调用完后修改ESP 比如 ADD ESP 12。PASCAL习惯的清楚堆栈和STDCALL一样。

在使用不同的语言设计的程序交流时可能会碰到这些问题。比如用VC++写的dll却不能在C++Builder中使用,很可能就是调用习惯的问题。

 

现在来用栈模拟函数调用消除递归。我们可能觉得递归函数和别的函数不同,但从CPU的角度来看它们没有什么区别。

我们来看一个简单的例子。

f(n)=n+1 当n<=1
f(n)=f([n/2]) * f([n/4]) 当n>=2

先写递归算法:

void fun(int n,int *result)
{
       if(n<=1)
       {
              *result=n+1;
       }
       else{
              int u1,u2;
              fun(n/2,&u1);  //第一个调用自己的地方,它返回后应该执行它下面的的语句。
              fun(n/4,&u2);  //第二个调用自己的地方,它返回后应该执行它下面的的语句。
              *result=u1*u2;
       }
}

 

非递归的算法怎么写呢?

再回忆一下前面的东西。函数调用做了三件事:传递参数(包括返回地址)并转到函数入口;获得参数并处理参数;根据传入的返回地址返回(返回前要清除堆栈)。现在这些工作不能指望编译器帮我们完成,自己动手才能丰衣足食了。先把代码贴到下面再解释。

void nonrec(int n,int*f){
       ELEM x,tmp;
       Stack stack;
       x.rd=3;x.pn=n;=f;
       stack.Push(x);
L0:
       stack.GetTop(x);
       if(x.pn<=1){
              *()=x.pn+1;
       }
       else{
              x.rd=1;x.pn=x.pn/2;
              =stack.GetTopQ1Addr();
              stack.Push(x);
              goto L0;
  
    
L1:
stack.GetTop(x);
              x.pn=x.pn/4;
              x.rd=2;
              =stack.GetTopQ2Addr();
              stack.Push(x);
              goto L0;
  
    
L2:
              stack.GetTop(x);
              *()=x.q1*x.q2;
       }
L3:
       stack.Pop(x);
       switch(x.rd){
       case 1:
              goto L1;
              break;
       case 2:
              goto L2;
              break;
       case 3:
              break;
       default:
              cout<<"error"<<endl;
              break;     
  
    
       }
  
    
}

由于运行时需要5个参数,n,*f,u1,u2,返回地址(rd)。每次传递时都push与pop5次也很麻烦,而且由于这些参数可能类型不同,所以把他们放到一个结构体中比较方便。

typedef struct {
       int rd,pn,*pf,q1,q2;
}ELEM;

最前面的一段:

  

ELEM x,tmp;
       Stack stack;
       x.rd=3;x.pn=n;=f;
       stack.Push(x);

这就是调用递归函数者首先要把参数放到“约定”的地点stack中。它的返回地址是3,我们并不关心返回地址的觉得内存地址,因为我们并不是真的像汇编语言一样JMP到那里去,我们只是要知道调用这个递归函数后应该执行下面的那条语句,所以只要用int类型区别一下就可以了。在fun函数中一共有两个地方调用自己用1,2表示。而最外面调用递归函数的用3来表示。

L0:

       stack.GetTop(x);

这段代码是说:L0就是函数的入口。进来的第一件事就是获得参数到x中。

在没有递归调用的地方,和递归函数的写法完全相同,只不过参数是用x中的变量而已。      

if(x.pn<=1){
              *()=x.pn+1;
       }
       else{

 这都和递归的写法一样。接着又要调用函数(自己)了。怎么调用函数呢?还是传递参数然后转到函数入口。

x.rd=1;x.pn=x.pn/2;
              =stack.GetTopQ1Addr();
              stack.Push(x);
              goto L0;

需要说明的是=stack.GetTopQ1Addr();的意思。我这里Stack的实现是用链表来实现的。struct ListNode{

ELEM data;
       ListNode *next;
};
class Stack{
private:
       ListNode *top;
……
       void GetTop(ELEM & elem){
              if(top==NULL) return ;
              elem=top->data;
       }
……..

把得到栈顶元素(不弹出)的方法是GetTop,它只是把栈顶元素复制一份到elem之中,而不是把栈顶元素的指针传出来。因为如果传指针出来的话似乎不太好,拿着指针的人修改了栈,栈自己却浑然不觉。因为是复制一份,所以我们给赋值时会遇到一个问题。本来应该指向x.u1的地址,也就是=&x.q1;但用于x是栈顶的拷贝,而我们想修改的是栈顶那个元素的u1。所以我打了个补丁,给栈多了两个方法GetTopQ1Addr()和GetTopQ2Addr()。代码写得实在很糟糕,主要是c++很久不用,写代码的时候没有考虑到这里还调试了老半天才发现这个bug,好在只有这部分是用c++实现的,所以大家凑合看看就得了。

然后下面应该第二次调用自己了:

L1:
              x.pn=x.pn/4;
              x.rd=2;
              =stack.GetTopQ2Addr();
              stack.Push(x);
              goto L0;

这和前面的代码基本类似。

第二次递归返回后的代码:

L2:
              *()=x.q1*x.q2;
       }

然后是函数结束,该返回了:

L3:
       stack.Pop(x);
       switch(x.rd){
       case 1:
              goto L1;
              break;
       case 2:
              goto L2;
              break;
       case 3:
              break;
       }

返回时应该根据x.rd判断应该goto到哪里。因为函数结束了,所以应该清除堆栈中的变量(它们的生命周期完了):stack.Pop(x);

注意每次要使用变量时都应该从stack中用stack.GetTop()获得。因为goto来goto去x可能已经不是当前的栈顶的元素了。

说明:事实上获取参数只要一次stack.GetTop()就可以了,例子中不行的原因是调用时把x改变了。比如:

L1:
              x.pn=x.pn/4;
              x.rd=2;
              =stack.GetTopQ2Addr();

如果用另一个变量来完成这些工作就不会用任何问题。

昨天没搞清楚就把“罪名”加到goto身上了。

此外我把Ackmann函数也用goto实现了非递归,有兴趣的话可以看看源代码。