关于尾递归【转】
尾递归是指具有如下形式的递归函数
f(x) ≡ if b(x) then h(x)
else f(k(x));
其中:
x, k: TYPE1, k(x) -< x ( 符号 -< 表示偏序)
h, f: TYPE2
b: boolean
且
b, h, k中都不含f
这样一个尾递归函数很容易转化为迭代。例如上述函数用C++语言写的递归代码为
T2 f(T1 x)
{
T1 x1;
if (b(x)) {
return h(x);
} else {
x1 = k(x);
return f(x1);
}
}
这里T1, T2是某个数据类型,b(x)是某个返回值为bool的函数,k(x)是某个返回值为T1的
函数,h(x)是某个返回值为T2的函数。显然函数f是一个递归函数,但因为他是尾递归,所
以很容易给改写成迭代:
T2 f(T1 x)
{
T1 x1;
loop:
if (b(x)) {
return h(x);
} else {
x1 = k(x);
x = x1; // 注意,这两行语句
goto loop; // 用goto把尾递归改为了迭代
}
}
然而通常所见到的递归都不是尾递归形式,这时候我们可以想办法用等价变换将其变化为
等价的尾递归形式。一个著名的等价变换就是cooper变换,其模式如下:
[Cooper变换]
输入模式:
f(x) ≡ if b(x) then h(x)
else F(f(k(x)), g(x))
输出模式:
f(x) ≡ G(x, e)
G(x, y) ≡ if b(x) then F(h(x), y)
else G( 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) ≡ if x = 0 then 1 else f(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) ≡ if x = 1 then 1 * y
else G(x-1, x * y);
用C++写的代码为:
int G(int x, int y)
{
int x1, y1;
if (x == 1) {
return 1 * y;
} else {
x1 = x - 1;
y1 = x *y;
return G(x1, y1);
}
}
int f(int x)
{
return G(x, 1);
}
其中尾递归函数G又可以进一步改写为迭代形式:
int G(int x, int y)
{
int x1, y1;
loop:
if (x == 1) {
return 1 * y;
} else {
x1 = x - 1;
y1 = x *y;
x = x1
y = y1;
goto loop;
}
}
另外还有几个常见的等价变换:
[拓广的Cooper变换]
输入模式:
f(x) ≡ if b(x) then h(x)
else if b1(x) then F1( f( k1(x) ), g1(x) )
...
else if bn(x) then Fn( f( kn(x) ), gn(x) )
else F0( f( k0(x) ), g0(x) )
输出模式:
f(x) ≡ if b(x) then h(x)
else if b1(x) then G1( k1(x), g1(x) )
...
else if bn(x) then Gn( kn(x), gn(x) )
else G0( k0(x), g0(x) )
对于所有的 0≤i≤n,
Gi( x, y) = if b(x) then Fi( h(x), y )
else if b1(x) then Gi( k1(x), F1( g1(x), y ) )
...
else if bn(x) then Gi( kn(x), Fn( gn(x), y ) )
else Gi( 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) ≡ if b(x) then h(x) else F( f(k(x)), g(x) )
输出模式:
f(x) ≡ G(x, x0, h(x0))
G(x, y, z) ≡ if y=x then z
else G(x, k'(y), F( z, g(k'(y))) )
可用性条件:
(1)b(x)为真时可求出相应之x值x0;
(2)k(x)存在反函数k'(x);
BTW: 迭代和递归的最大区别就是迭代的空间复杂度为O(1),即所谓的constant space。
哪种用堆栈来模拟递归的方法,本质上还是递归
只不过人工做了本来由编译器做的事情
只要使用了对栈,空间复杂度通常就和输入规模n有关,而不可能是常数了
这个翻筋斗是说的是所谓 trampolined style 这样的一种编程技巧。
这个技巧在做尾递归消除的时候特别有用。
我们知道 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 语言里面并没有提供这样尾递归消除的机制。
这就只好依靠程序员自己想办法了。
最早这个办法是 Guy L. Steele 在 Rabbit 那篇 Scheme 的论文
里面想到的。后来 Philip Wadler 和 Simon Peyton Jones 等人
在 Glasgow Haskell 项目里面也又独立的把这个方法发明了一遍。
这个方法基本上说来,就是让程序的主体部分在一个
循环里面运行一个调度程序,
while (1) { cont = (*cont)(); }
让每一个普通的函数返回的时候,设置一个全局变量,
记录下一步继续执行那一个函数。这个 继续 在这里就可以
当一个名词来使用,是不是就让你想到
scheme 语言当中大名鼎鼎的 continuation 啊?:)
还有其它种类的翻筋斗。上面说的这个翻筋斗,
如果自己手写,其实也不是多古怪。不过终归是不太好,
这也就是语言和语言之间的一个区别,或者也许也可以说是
目前的语言都是要么这样要么那样的不能令人满意吧。
不过这个我们以后再慢慢说吧。
Steele 和 Peyton Jones 和 Philip Wadler 他们
是把 scheme / haskell 编译成 c 语言,也就是说
他们的这个翻筋斗不是手写的,是个编译到
c 语言的技巧。所以古怪不古怪对它们来说
就不成问题啦。
在 Daniel Friedman 和几个人和写的那篇
专门谈论翻筋斗的文章中,还有一些更喏嗦的内容。
首先我们看到上面的这个技巧可以用来在
自己的 c 程序当中实现一个非抢占式的多任务系统。
Mitch Wand 后来有一篇论文讲到在 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