递归与迭代---用迭代实现递归

(递归和迭代 is over due)

Submit TimeProblem NameJudge ResultLanguage
 

递归和迭代

递归函数的优点在于程序很简洁,但是效率往往比较低。此时我们可以把递归函数转换成为循环,以迭代的方式实现递归函数。实际上,我们在前面已经做过一次练习了。

对于尾递归函数,我们可以把递归方便地转换为迭代。所谓尾递归,就是说递归函数具有这样的形式:

T    f(T’ args)
{
    If(args满足退出条件)
        return X;
    …..//进行一系列计算;
    return f(args’);
}

那么我们可以很方便地转成下面的while语句。方法是设立一个循环,并设立一个变量来存放实在参数。在原程序返回的地方,把结果赋给相应的变量并退出循环。在原来递归调用的地方把新的参数赋给存放实在参数的变量,并continue循环。

V = args;
for(;;)
{
    if(V 满足退出条件)
    {
        result = X;
        break;
    }
    ….//进行同样的计算;
    V = args’;
}

注意,只要程序总是把递归调用得到的值直接返回,就可以称为尾递归,并不一定要求只递归调用一次。大家可以看第一个题目的例子。 我们经常把计算阶乘的程序看作尾递归;

f(n) { return n*f(n-1); }

但是严格地讲它不是尾递归,因为f(n-1)的返回值还需要和n相乘然后返回。不过,因为乘法交换率,我们还是很容易把它写成尾递归。

题目1:尾递归到迭代的转换

在一个排好序的数组A中搜索某个数x,判断是否在这个数组中,我们可以用二分法进行搜索。递归程序如下:

bool Find(int iL, int iR, x)
{
    if(iR < iL)
    return false;
    int tmp = A[(iL+iR)/2];
    if(tmp == x)
        return true;
    else if(tmp < x)
        return Find((iL+iR)/2+1, iR);
    else
        return Find(iL, (iL+iR)/2-1);
}

请把这个尾递归函数转换成为一个迭代函数;

输入

多行,每行的第一个数字是要搜索的x;第二个数字是数组中非负整数的个数n(n<1000), 接下来的n各数字就是数组中的数; 最后一行以-1开始,表示输入结束, 不需要处理;

输出

对于每行输入, 有一行输出; 如果x在数组中,则输出Yes, 否则输出No;

例子输入

3 5 1 2 3 4 5
5 7 1 2 3 4 6 7 8
-1

例子输出

Yes
No


关于尾递归【转】
尾递归是指具有如下形式的递归函数



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 的文章里面似乎还有点别的内容。

我不过我就没仔细看了。

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




题目2:栈的实现

借助栈(大家可以到网上或者教科书上去看一下栈的定义)这个抽象数据结构,我们很容易把递归函数转换成为迭代函数。下面我们首先用数组来实现一个栈。这次要求大家实现一个类(如果大家还没有学习面向对象编程,现在可以去学习一下)。要求:使用数组实现类ArrayStack;这个栈中最多可以容纳1000个整数类型的元素;类ArrayStack包含如下操作:

  1. void Empty():清空栈中的所有元素;
  2. void Pop(): 弹出一个元素
  3. int Top(); 获取栈顶的元素;
  4. void push(int x);将x压到栈顶;
  5. void output;从栈顶开始,输出栈中的所有元素。

输入

多行;每行一个命令,命令格式如下:

命令 要求 0 清空栈中内容; 1 弹出一个元素; 2 获取栈顶元素,并用单独一行输出x; 3 x 将x压到栈顶; 4 从栈顶开始输出所有元素。所以的输出在一行上。

在输入中,保证第一个命令是0;且保证处理过程中不会对空栈执行pop操作,或者对满栈执行push操作(保证栈中元素小于1000个)。

在输出所有元素时,如果栈中没有元素,则输出空行。

输入

多行,每行一个命令。命令格式见上。

输出:按照命令要求输出。

例子输入

0
3 1
3 2
3 3
4
1
2
1
4

例子输出

3 2 1
2
1

利用栈把递归函数改成迭代函数

在高级语言中的函数实际上是通过栈来实现的。在每次调用的开始,调用者把实在参数、返回地址压入栈中,并为被调用者的局部变量等分配空间。然后跳转到被调用者;被调用者执行完毕后,退栈,并且根据之前压入的返回地址跳转到调用者的后继代码继续运行。这些操作对于高级程序设计语言的程序员来说是透明的。现在我们可以自己声明一个栈,然后自己通过压栈/出栈来模拟递归调用。这么做的好处在于我们可以进行进一步优化。

一般来说,转换的方法可以是这样的:

假设有递归函数F, 它的返回类型是T0,有T1类型的参数x,两个局部变量locVar1, locVar2。多次调用自己。

T0  F(T1  x)
{
    T2  locVar1, locVar2;
    ...//运算0;
    if(…)
        return …
    …//运算1;
    F(x1);//第一次调用;
    …//运算2;
    F(x2);//第二次调用;
    …
}

那么,我们可以声明一个栈,栈中元素的类型是:

struct  element {
    T0* ret;        //保存返回值的地址;
    T1 x;           //对应于实在参数
    T2 locVar1, locVar2;        //对应于局部变量;
T0  ret1, ret2;         //用于存放第一次和第二次递归调用的返回值;
int Pos;
}

其中Pos字段记录了调用者调用本函数的位置。 然后处理的方法如下: 1、 使用入栈/出栈来模拟递归调用/返回 1、 调用:设置栈顶的进度标记(记住是第几次调用);设置参数、返回值地址,并入栈 2、 返回:将返回值根据返回值地址拷贝到相应位置,出栈, 3、 其他计算过程:根据当前进度标记执行;所有对实在参数/局部变量的访问 2、 使用一个循环来模拟执行过程; a) 根据当前栈顶记录中的信息决定下一步执行什么操作;

比如

long Fib(long n) { if (n <= 1) return n; else return Fib(n-1)+Fib(n-2); }

可以设立一个栈,栈元素类型是

struct Node{
    long n;         //参数
    long t1;        //局部变量,存第一次调用Fib的返回值
    long t2;        //局部变量,存第二次调用Fib的返回值
    long* ret;      //指向存放返回值的地址
    int IP;     //0表示刚开始执行;
            //1表示调用了第一次Fib,
            //2表示第二次调用Fib
}

而相应的迭代代码如下:(红色的注释是迭代程序中的对应代码)

long Fib(int n)
{   long ret;
    stack<Node>  s; Node *w;
    Node record = {n,0,0,&ret,0};
    s.push(bottom);
    while(!s.IsEmpty())
    {
        Node* curRec = s.GetTopPointer();
        //根据curRec->的IP,决定执行相应的代码 
        switch(curRec->IP)
        {case 0: 
            if(curRec->n <= 1) // if (n <= 1) return n; 
            {  *curRec->ret = curRec->n;  s.Pop();} 
            else 
            { curRec->IP=1;  //记住已经执行到第一次调用之前; 
              s.Push({curRec->n-1,0,0,&curRec->t1,0});} // t1=Fib(n-1); 
            break;
        case 1:   curRec->IP=2; 
            s.Push({curRec->n-2,0,0,&curRec->t2, 0}); //t2=Fib(n-2); 
            break;
        case 2:   *curRec->ret = curRec->t1 + curRec->t2; //return t1+t2;
            s.Pop(); //返回
    }
    return ret;
}

这个代码并不是高效的代码,但是我们可以进一步优化。比如,对于case 1,我们压栈之后,下一轮迭代会把刚刚压栈的元素出栈。因此,我们可以直接进行计算,而免除相应的压栈/出栈操作。

题目3:QuickSort的迭代实现

QuickSort是一个很高效的排序算法;大家可以在网络上搜索到它的递归实现。现在请大家利用栈把它改写成为迭代程序。大家可以考虑高效一点的转换:

  1. 可以考虑不把局部变量记录到栈中;
  2. QuickSort函数没有返回值; 3 .QuickSort递归调用自身两次;但是后一次类似于尾递归,可以按照尾递归进行处理;这样我们就只需要把第一次调用对应的记录压栈;栈中所有记录的pos值都是一样的。因此我们也可以省略这个值;

因此,栈中只需要记录实在参数。

输入

多行,每行的第一个整数表明待排序的整数的个数,这一行的其余部分就是这个整数。

被排序的整数个数不多于9999;

最后一行以-1开始,不需要处理。

输出

对于每一行输入,输出从小到大排列的结果。数字之间用一个空格隔开。

注意:这个题目的目的是练习递归到迭代的转换,所以不要使用其它的算法。要求使用迭代+栈的实现方式.

例子输入

5 1 2 3 5 4
-1

例子输出

1 2 3 4 5
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值