04 消除递归

 

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

要说明一下的是,我说的栈就是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);

…….

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

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

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

我们编写函数sum时,拿着ab就使用。如果让我们自己传递参数,会怎么样呢?回忆一下在汇编语言中传递参数的方法。就是函数调用者和函数“约定”一个内存空间(或者寄存器)来存放参数。调用者先把参数放到约定好的地方,然后就准备调用函数,调用其实就是把函数的地址放到IP寄存器,当然为了能够调用后返回,还得把调用者调用完函数后的下一条指令的地址放到一个“约定”的位置,如果使用CALLRET指令,这些工作它就帮你做了。还有一个要说明的是,在汇编语言中是没有返回值这个概念的。所谓的返回值其实就是对一个变量的修改。比如c=int(2,3);就是要根据输入参数23,修改c的值,在汇编语言中实现的方法是把c的地址作为一个参数放到一个约定的地点,sum函数把23的结果放到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函数,参数是34&c,调用后转到某个地方”。它该怎么办呢?INVOKE指令就是用来干这个的。CALL是没有参数的,你不能             CALL (3,4,&c),传递34&c的任务是由调用者完成的,就想代码中的调用前得自己把他们压入stack。而INVOKE就把这些工作做好了,你如果使用INVOKE 34&c,它就会把34&c压入。

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

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

       stack.Push(x);

L0:

       stack.GetTop(x);

       if(x.pn<=1){

              *(x.pf)=x.pn+1;

       }

       else{

              x.rd=1;x.pn=x.pn/2;

              x.pf=stack.GetTopQ1Addr();

              stack.Push(x);

              goto L0;

 

L1:

stack.GetTop(x);

              x.pn=x.pn/4;

              x.rd=2;

              x.pf=stack.GetTopQ2Addr();

              stack.Push(x);

              goto L0;

 

L2:

              stack.GetTop(x);

              *(x.pf)=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)。每次传递时都pushpop5次也很麻烦,而且由于这些参数可能类型不同,所以把他们放到一个结构体中比较方便。

typedef struct {

       int rd,pn,*pf,q1,q2;

}ELEM;

最前面的一段:

       ELEM x,tmp;

       Stack stack;

       x.rd=3;x.pn=n;x.pf=f;

       stack.Push(x);

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

L0:

       stack.GetTop(x);

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

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

if(x.pn<=1){

              *(x.pf)=x.pn+1;

       }

       else{

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

              x.rd=1;x.pn=x.pn/2;

              x.pf=stack.GetTopQ1Addr();

              stack.Push(x);

              goto L0;

需要说明的是x.pf=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.pf赋值时会遇到一个问题。本来x.pf应该指向x.u1的地址,也就是x.pf=&x.q1;但用于x是栈顶的拷贝,而我们想修改的是栈顶那个元素的u1。所以我打了个补丁,给栈多了两个方法GetTopQ1Addr()GetTopQ2Addr()。代码写得实在很糟糕,主要是c++很久不用,写代码的时候没有考虑到这里还调试了老半天才发现这个bug,好在只有这部分是用c++实现的,所以大家凑合看看就得了。

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

L1:

              x.pn=x.pn/4;

              x.rd=2;

              x.pf=stack.GetTopQ2Addr();

              stack.Push(x);

              goto L0;

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

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

L2:

              *(x.pf)=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()获得。因为gotogotox可能已经不是当前的栈顶的元素了。

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

L1:

              x.pn=x.pn/4;

              x.rd=2;

              x.pf=stack.GetTopQ2Addr();

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

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

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值