简体   繁体   English

将递归转换为“尾递归”

[英]convert recursion to 'tail recursion'

I have a question about how to convert 'recursion' to 'tail recursion'.我有一个关于如何将“递归”转换为“尾递归”的问题。 this is not a homework, just a question pop up when I tried to polish the recursion theorem from algorithm book.这不是作业,只是当我试图完善算法书中的递归定理时弹出一个问题。 I am familiar with the 2 typical examples of using recursion(factorial and Fibonacci sequence), and also know how to implement them in recursive way and tail-recursive way.我熟悉使用递归的2个典型示例(阶乘和斐波那契数列),也知道如何以递归方式和尾递归方式实现它们。 My code is as below(I use Perl just to make it simple, but can be easily convert to C/Java/C++)我的代码如下(我使用 Perl 只是为了简单起见,但可以轻松转换为 C/Java/C++)

#this is the recursive function
sub recP    {
    my ($n) = @_;
    if ($n==0 or $n==1 or $n==2)    {
        return 1;
    } else {
        return (recP($n-3)*recP($n-1))+1;
    }

}
for (my $k=1;$k<10;$k++) {
    print "*"x10,"\n";
    print "recP($k)=", recP($k), "\n";
}

When running the code, the output as below:运行代码时,输​​出如下:

recP(1)=1 
recP(2)=1 
recP(3)=2 
recP(4)=3 
recP(5)=4 
recP(6)=9 
recP(7)=28 
recP(8)=113 
recP(9)=1018 

the recursive function invoke itself twice with different parameter before return;递归函数在返回之前用不同的参数调用自己两次; I tried several ways to convert this to a tail recursive way but all turns out wrong.我尝试了几种方法将其转换为尾递归方式,但结果都是错误的。

Can anybody take a look at the code and show me the correct way to make it tail-recursive?任何人都可以看看代码并向我展示使其尾递归的正确方法吗? especially I believe there is a routine for the conversion for this tree recursion(invoke recursive function multiple times before return), can any shed some light on this?特别是我相信有一个用于此树递归转换的例程(在返回之前多次调用递归函数),是否可以对此有所了解? So I can use the same logic to handle different questions later.所以我以后可以用同样的逻辑来处理不同的问题。 thanks in advance.提前致谢。

Although you often see the following as an example of converting factorial to tail-call:虽然您经常看到以下作为将阶乘转换为尾调用的示例:

int factorial(int n, int acc=1) {
  if (n <= 1) return acc;
  else        return factorial(n-1, n*acc);
}

it's not quite correct, since it requires multiplication to be both associative and commutative.它不太正确,因为它要求乘法既是结合又是可交换的。 (Multiplication is associative and commutative, but the above doesn't serve as a model for other operations which don't satisfy those constraints.) A better solution might be: (乘法结合的和可交换的,但以上不能作为不满足这些约束的其他操作的模型。)更好的解决方案可能是:

int factorial(int n, int k=1, int acc=1) {
  if (n == 0) return acc;
  else        return factorial(n-1, k+1, acc*k);
}

This also serves as a model for the fibonacci transform:这也用作斐波那契变换的模型:

int fibonacci(int n, int a=1, int b=0) {
  if (n == 0) return a;
  else        return fibonacci(n-1, a+b, a);
}

Note that these compute the sequence starting at the beginning, as opposed to queueing pending continuations in a call stack.请注意,这些计算从开头开始的序列,而不是在调用堆栈中排队挂起的延续。 So they are structurally more like the iterative solution than the recursive solution.所以它们在结构上更像是迭代解决方案而不是递归解决方案。 Unlike the iterative program, though, they never modify any variable;然而,与迭代程序不同的是,它们从不修改任何变量; all bindings are constant.所有绑定都是恒定的。 This is an interesting and useful property;这是一个有趣且有用的属性; in these simple cases it doesn't make much difference, but writing code without reassignments makes some compiler optimizations easier.在这些简单的情况下,它没有太大区别,但是编写没有重新分配的代码会使一些编译器优化更容易。

Anyway, the last one does provide a model for your recursive function;无论如何,最后一个确实为您的递归函数提供了一个模型; like the fibonacci sequence, we need to keep the relevant past values, but we need three of them instead of two:就像斐波那契数列一样,我们需要保留过去的相关值,但我们需要三个而不是两个:

int mouse(int n, int a=1, int b=1, int c=1) {
  if (n <=2 ) return a;
  else        return mouse(n-1, a*c+1, a, b);
}

Addenda附加物

In comments, two questions were raised.在评论中,提出了两个问题。 I'll try to answer them (and one more) here.我会试着在这里回答他们(还有一个)。

First, it should be clear (from a consideration of the underlying machine architecture which has no concept of function calling) that any function call can be rephrased as a goto (possibly with non-bounded intermediate storage);首先,应该清楚(从没有函数调用概念的底层机器架构的考虑)任何函数调用都可以改写为 goto(可能具有无界中间存储); furthermore, any goto can be expressed as a tail-call.此外,任何 goto 都可以表示为尾调用。 So it is possible (but not necessarily pretty) to rewrite any recursion as tail-recursion.所以有可能(但不一定漂亮)将任何递归重写为尾递归。

The usual mechanism is "continuation-passing style" which is a fancy way of saying that every time you want to call a function, you instead package the rest of the current function as a new function (the "continuation"), and pass that continuation to the called function.通常的机制是“continuation-passing style”,这是一种奇特的说法,每次你想调用一个函数时,你将当前函数的其余部分打包为一个新函数(“continuation”),然后传递继续调用的函数。 Since every function then receives a continuation as an argument, it has to finish any continuation it creates with a call to the continuation it received.由于每个函数都接收一个延续作为参数,它必须通过调用它接收到的延续来完成它创建的任何延续。

That's probably enough to make your head spin, so I'll put it another way: instead of pushing arguments and a return location onto the stack and calling a function (which will later return), you push arguments and a continuation location onto the stack and goto a function, which will later goto the continuation location.这可能足以让你头晕目眩,所以我换一种说法:不是将参数和返回位置推入堆栈并调用函数(稍后将返回),而是将参数和继续位置推入堆栈并转到一个函数,该函数稍后将转到继续位置。 In short, you simply make the stack an explicit parameter, and then you never need to return.简而言之,您只需将堆栈设置为显式参数,然后就无需返回。 This style of programming is common in event-driven code (see Python Twisted), and it's a real pain to write (and read).这种编程风格在事件驱动代码中很常见(参见 Python Twisted),编写(和阅读)真的很痛苦。 So I strongly recommend letting compilers do this transformation for you, if you can find one which will do that.所以我强烈建议让编译器为你做这个转换,如果你能找到一个可以做到的。

@xxmouse suggested that I pulled the recursion equation out of a hat, and asked how it was derived. @xxmouse建议我从帽子里拿出递归方程,并询问它是如何推导出来的。 It's simply the original recursion, but reformulated as a transformation of a single tuple:它只是原始递归,但重新表述为单个元组的转换:

f n = f n-1 *f n-3 + 1
=>
F n = <F n-1 1 *F n-1 3 +1, F n-1 1 , F n-1 2 >

I don't know if that's any clearer, but it's the best I can do.我不知道这是否更清楚,但这是我能做的最好的。 Look at the fibonacci example for a slightly simpler case.查看斐波那契示例以了解稍微简单的情况。

@j_random_hacker asks what the limits on this transformation are. @j_random_hacker询问这种转换的限制是什么。 It works for a recursive sequence where each element can be expressed by some formula of the previous k elements, where k is a constant.它适用于递归序列,其中每个元素都可以由前k元素的某个公式表示,其中k是一个常数。 There are other ways to produce a tail-call recursion.还有其他方法可以产生尾调用递归。 For example:例如:

// For didactic purposes only
bool is_odd(int n) { return n%2 == 1; }

int power(int x, int n, int acc=1) {
  if (n == 0)         return acc;
  else if (is_odd(n)) return power(x, n-1, acc*x);
  else                return power(x*x, n/2, acc);
}

The above is not the same as the usual non-tail-call recursion, which does a different (but equivalent and equally long) sequence of multiplications.上面的是一样的通常的非尾调用递归,它不乘法的不同(但等效的和等长的)序列。

int squared(n) { return n * n; }

int power(int x, int n) {
  if (n == 0)         return 1;
  else if (is_odd(n)) return x * power(x, n-1));
  else                return squared(power(x, n/2));
}

Thanks to Alexey Frunze for the following test: Output ( ideone ):感谢 Alexey Frunze 进行以下测试:输出( ideone ):

mouse(0) = 1
mouse(1) = 1
mouse(2) = 1
mouse(3) = 2
mouse(4) = 3
mouse(5) = 4
mouse(6) = 9
mouse(7) = 28
mouse(8) = 113
mouse(9) = 1018

Using google, I found this page that describes Tail Recursion .使用谷歌,我找到了描述Tail Recursion 的页面。 Basically, you need to split the function into at least two other functions: one that does the work, keeping an "accumulation" of the current value, and another that is a driver for your workhouse function.基本上,您需要将该函数拆分为至少两个其他函数:一个执行工作,保持当前值的“累积”,另一个是您的工作室功能的驱动程序。 The factorial example in C is below: C中的阶乘示例如下:

/* not tail recursive */
unsigned int
factorial1(unsigned int n)
{
    if(n == 0)
        return 1;
    return n * factorial1(n-1);
}

/* tail recursive version */
unsigned int 
factorial_driver(unsigned int n, unsigned int acc)
{
    if(n == 0)
        return acc;

    /* notice that the multiplication happens in the function call */
    return factorial_driver(n - 1, n * acc);
}

/* driver function for tail recursive factorial */
unsigned int
factorial2(unsigned int n)
{
    return factorial_driver(n, 1);
}

@Alexey Frunze's answer is okay but not precisely right. @Alexey Frunze 的回答没问题,但不完全正确。 It is indeed possible to convert any program into one where all recursion is tail recursion by transforming it into Continuation Passing Style .通过将任何程序转换为Continuation Passing Style ,确实可以将任何程序转换为所有递归都是尾递归的程序。

I don't have time right now but will try to re-implement your program in CPS if I get some minutes.我现在没有时间,但如果我有时间,我会尝试在 CPS 中重新实施您的程序。

You could do something like this:你可以这样做:

#include <stdio.h>

void fr(int n, int a[])
{
  int tmp;

  if (n == 0)
    return;

  tmp = a[0] * a[2] + 1;
  a[2] = a[1];
  a[1] = a[0];
  a[0] = tmp;

  fr(n - 1, a);
}

int f(int n)
{
  int a[3] = { 1, 1, 1 };

  if (n <= 2)
    return 1;

  fr(n - 2, a);

  return a[0];
}

int main(void)
{
  int k;
  for (k = 0; k < 10; k++)
    printf("f(%d) = %d\n", k, f(k));
  return 0;
}

Output ( ideone ):输出( ideone ):

f(0) = 1
f(1) = 1
f(2) = 1
f(3) = 2
f(4) = 3
f(5) = 4
f(6) = 9
f(7) = 28
f(8) = 113
f(9) = 1018

The compiler may transform fr() into something like this:编译器可能会将fr()转换成这样的:

void fr(int n, int a[])
{
  int tmp;

label:    

  if (n == 0)
    return;

  tmp = a[0] * a[2] + 1;
  a[2] = a[1];
  a[1] = a[0];
  a[0] = tmp;

  n--;

  goto label;
}

And that would be tail-call optimization.这将是尾调用优化。

The problem is that the last operation is not one of the recursive calls, but the addition of 1 to the multiplication.问题是最后一个操作不是递归调用,而是乘法加1。 Your function in C:您在 C 中的功能:

unsigned faa (int n)  // Ordinary recursion
{
    return n<3 ? 1 :
                 faa(n-3)*faa(n-1) + 1;  // Call, call, multiply, add
}

If you change the order in which the values are requested, you can turn one of the calls into a loop:如果更改请求值的顺序,则可以将其中一个调用转换为循环:

unsigned foo (int n)  // Similar to tail recursion
{                     // (reverse order)
    int i;
    unsigned f;

    for (i=3, f=1; i<=n; i++)
        f = f*foo(i-3) + 1;

    return f;
}

The key is thinking about the order in which values are actually computed in the original function, instead of the order in which they are requested.关键是考虑在原始函数中实际计算值的顺序,而不是请求它们的顺序。

Note that I am assuming that you want to remove one recursive call.请注意,我假设您要删除一个递归调用。 If you want to write the recursive call at the end of the function expecting the compiler to optimize it away for you, see the other answers.如果您想在期望编译器为您优化的函数末尾编写递归调用,请参阅其他答案。

Though, "The Right Thing(TM)" to do here is to use dynamic programming to avoid computing the same values many times:但是,这里要做的“正确的事情(TM)”是使用动态编程来避免多次计算相同的值:

unsigned fuu (int n)  // Dynamic programming
{
    int i;
    unsigned A[4]={1,1,1,1};

    for (i=3; i<=n; i++)
    {
        memmove (A+1, A, 3*sizeof(int));
        A[0] = A[1]*A[3] + 1;
    }

    return A[0];
}

The array A contains a sliding window of the sequence: A[0]==f(i), A[1]==f(i-1), A[2]==f(i-2) and so on.数组 A 包含序列的滑动窗口:A[0]==f(i), A[1]==f(i-1), A[2]==f(i-2) 等等.

The memmove might have been written as: memmove可能写成:

        A[3] = A[2];
        A[2] = A[1];
        A[1] = A[0];

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM