关于尾递归【转】

 

关于尾递归【转】

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

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

你可能感兴趣的:(多线程,c,算法,Scheme,语言,编译器)