您的位置:首页 > 其它

关于尾递归【转】

2009-09-16 11:05 10 查看

关于尾递归【转】

尾递归是指具有如下形式的递归函数

f(x)≡ifb(x)thenh(x)

elsef(k(x));

其中:

x,k:TYPE1,k(x)-<x(符号-<表示偏序)

h,f:TYPE2

b:boolean

且

b,h,k中都不含f

这样一个尾递归函数很容易转化为迭代。例如上述函数用C++语言写的递归代码为

T2f(T1x)

{

T1x1;

if(b(x)){

returnh(x);

}else{

x1=k(x);

returnf(x1);

}

}

这里T1,T2是某个数据类型,b(x)是某个返回值为bool的函数,k(x)是某个返回值为T1的

函数,h(x)是某个返回值为T2的函数。显然函数f是一个递归函数,但因为他是尾递归,所

以很容易给改写成迭代:

T2f(T1x)

{

T1x1;

loop:

if(b(x)){

returnh(x);

}else{

x1=k(x);

x=x1;//注意,这两行语句

gotoloop;//用goto把尾递归改为了迭代

}

}

然而通常所见到的递归都不是尾递归形式,这时候我们可以想办法用等价变换将其变化为

等价的尾递归形式。一个著名的等价变换就是cooper变换,其模式如下:

[Cooper变换]

输入模式:

f(x)≡ifb(x)thenh(x)

elseF(f(k(x)),g(x))

输出模式:

f(x)≡G(x,e)

G(x,y)≡ifb(x)thenF(h(x),y)

elseG(k(x),F(g(x),y))

其中:

x,k:TYPE1,k(x)-<x(符号-<表示偏序)

y,G,h,g,f,F:TYPE2

b:boolean

e:F的右单位元,即F(x,e)=x

可用性条件:

(1)F满足结合律,即F(F(x,y),z)=F(x,F(y,z))

(2)F有右单位元e;

(3)b,h,g,k中都不含f

例如考虑计算阶乘的函数

f(x)≡ifx=0then1elsef(x-1)*x;

对照cooper变换,易见该函数是满足cooper变换的输入模式和适用性条件的。其中

b(x)≡(x=1);

h(x)≡1

F(x,y)≡x*y,F的单位元e=1

k(x)≡x-1

g(x)≡x

于是我们可以根据cooper变换将f(x)改写为:

f(x)≡G(x,1);

G(x,y)≡ifx=1then1*y

elseG(x-1,x*y);

用C++写的代码为:

intG(intx,inty)

{

intx1,y1;

if(x==1){

return1*y;

}else{

x1=x-1;

y1=x*y;

returnG(x1,y1);

}

}

intf(intx)

{

returnG(x,1);

}

其中尾递归函数G又可以进一步改写为迭代形式:

intG(intx,inty)

{

intx1,y1;

loop:

if(x==1){

return1*y;

}else{

x1=x-1;

y1=x*y;

x=x1

y=y1;

gotoloop;

}

}

另外还有几个常见的等价变换:

[拓广的Cooper变换]

输入模式:

f(x)≡ifb(x)thenh(x)

elseifb1(x)thenF1(f(k1(x)),g1(x))

...

elseifbn(x)thenFn(f(kn(x)),gn(x))

elseF0(f(k0(x)),g0(x))

输出模式:

f(x)≡ifb(x)thenh(x)

elseifb1(x)thenG1(k1(x),g1(x))

...

elseifbn(x)thenGn(kn(x),gn(x))

elseG0(k0(x),g0(x))

对于所有的0≤i≤n,

Gi(x,y)=ifb(x)thenFi(h(x),y)

elseifb1(x)thenGi(k1(x),F1(g1(x),y))

...

elseifbn(x)thenGi(kn(x),Fn(gn(x),y))

elseGi(k0(x),F0(g0(x),y))

其中:

对于所有的0≤i≤n

x,ki:TYPE1,ki(x)-<x(符号-<表示偏序)

gi,h,Fi,Gi,y:TYPE2

b,bj:boolean,1≤j≤n

b(x)∧b1(x)∧……∧bn(x)=φ(空集)

b(x)∨b1(x)∨……∨bn(x)=Ω(全集)

可用性条件:

(1)Fi满足结合律,即Fi(Fj(x,y),z)=Fj(x,Fi(y,z)),0≤i,j≤n

(2)b,bj,h,gi,ki中都不含f,0≤i≤n,1≤j≤n

[反演变换]

输入模式:

f(x)≡ifb(x)thenh(x)elseF(f(k(x)),g(x))

输出模式:

f(x)≡G(x,x0,h(x0))

G(x,y,z)≡ify=xthenz

elseG(x,k'(y),F(z,g(k'(y))))

可用性条件:

(1)b(x)为真时可求出相应之x值x0;

(2)k(x)存在反函数k'(x);

BTW:迭代和递归的最大区别就是迭代的空间复杂度为O(1),即所谓的constantspace。

哪种用堆栈来模拟递归的方法,本质上还是递归

只不过人工做了本来由编译器做的事情

只要使用了对栈,空间复杂度通常就和输入规模n有关,而不可能是常数了

这个翻筋斗是说的是所谓trampolinedstyle这样的一种编程技巧。

这个技巧在做尾递归消除的时候特别有用。

我们知道c语言里面用堆栈来实现递归。每进行一次函数调用,

调用堆栈都会长一点,把一些必要的信息记下来,比如当

被调用的函数结束的时候,如何返回调用函数,它的执行地址

在哪里等等。

所谓递归,就是函数在执行过程中,会调用到自己,

一般正常的情况下,每次递归调用都是用不同的函数参数

来进行的。一般来说,这样每一次要进行的计算

比起上一次来说,就会简单一点。这样达到一个地步,

到了这个地步就不用再调用自己,直接就能给出答案了。

这个时候,堆栈上积累了一长串调用函数的脚印,

最简单的情况得到答案以后,我们就顺着这串脚印,

倒着走回去,每走回去一步,就是回到上一级的调用函数,

给出稍微复杂一点的那个问题的答案。这样一步步的

返回去,我们就得到了原来问题的答案。

也就是说,我们用堆栈实现了一个递归算法,完成了我们的问题。

所谓尾递归,函数运行过程中会调用自己,

我们把当前的这个运算过程叫做A。它会调用自己

展开一个新的计算过程,我们把它记做B。

一般的递归运算,在B结束运算,得到一个阶段性的结果以后,

在返回到计算过程A以后,还需要用B的计算结果,

再做一些处理,然后才能结束A的运算,把结果返回到

递归调用的上一级。

所谓尾递归的情况,就是说在B结束,返回到A以后,

A对B的运算结果不做任何进一步的处理,就把结果

直接返回到上一级。这就是所谓在结尾处进行的递归。

显然我们能看出来,在尾递归的情况下,

我们不许要增长堆栈。因为从B返回以后,

我们就直接从A返回,中间没有停顿。

这样在调用B的时候,我们就不需要在堆栈上留下

A的印迹。要知道,我们原先之所以需要这个印迹,

是因为我们还要凭借这个印迹回到A

再做一点运算,才能回到A的上一级。现在

尾递归的情况,我们不需要回到A,直接就可以从

B回到A的上一级。这样在A调用B的时候,

我们原来需要A在堆栈上留个印迹,现在我们就不需要了。

我们希望把A就此忘掉,不想让它增长我们的堆栈。

而这应该是完全可以达到的目的。

不过c语言里面并没有提供这样尾递归消除的机制。

这就只好依靠程序员自己想办法了。

最早这个办法是GuyL.Steele在Rabbit那篇Scheme的论文

里面想到的。后来PhilipWadler和SimonPeytonJones等人

在GlasgowHaskell项目里面也又独立的把这个方法发明了一遍。

这个方法基本上说来,就是让程序的主体部分在一个

循环里面运行一个调度程序,

while(1){cont=(*cont)();}

让每一个普通的函数返回的时候,设置一个全局变量,

记录下一步继续执行那一个函数。这个继续在这里就可以

当一个名词来使用,是不是就让你想到

scheme语言当中大名鼎鼎的continuation啊?:)

还有其它种类的翻筋斗。上面说的这个翻筋斗,

如果自己手写,其实也不是多古怪。不过终归是不太好,

这也就是语言和语言之间的一个区别,或者也许也可以说是

目前的语言都是要么这样要么那样的不能令人满意吧。

不过这个我们以后再慢慢说吧。

Steele和PeytonJones和PhilipWadler他们

是把scheme/haskell编译成c语言,也就是说

他们的这个翻筋斗不是手写的,是个编译到

c语言的技巧。所以古怪不古怪对它们来说

就不成问题啦。

在DanielFriedman和几个人和写的那篇

专门谈论翻筋斗的文章中,还有一些更喏嗦的内容。

首先我们看到上面的这个技巧可以用来在

自己的c程序当中实现一个非抢占式的多任务系统。

MitchWand后来有一篇论文讲到在scheme里面

如何用continuation实现多线程,

大体上似乎是一个意思。不过Dybvig似乎有一个

抢占式的多线程的实现方法,我老早以前看的,

当时就没明白。现在对这个话题不是特别感兴趣。

(一般来说,我对用到很强的技巧的东西都不感兴趣,呵呵)

对了,在Knuth在TAOCP第一卷里面谈到过coroutine

这些也是相关的内容。

还有Moggi有Monadic的变化。这个我还不甚了了。

Friedman的文章里面似乎还有点别的内容。

我不过我就没仔细看了。

如果你看到别的内容,麻烦你也告诉我一声喽。:)





转自:http://bbs.ustc.edu.cn/cgi/bbsanc?path=/groups/GROUP_5/PLTheory/D71A0CD5A/M.1082983891.A

[/code]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: