简体   繁体   English

将正常递归转换为尾递归

[英]Convert normal recursion to tail recursion

I was wondering if there is some general method to convert a "normal" recursion with foo(...) + foo(...) as the last call to a tail-recursion. 我想知道是否有一些通用方法来转换“正常”递归与foo(...) + foo(...)作为最后一次调用尾递归。

For example (scala): 例如(scala):

def pascal(c: Int, r: Int): Int = {
 if (c == 0 || c == r) 1
 else pascal(c - 1, r - 1) + pascal(c, r - 1)
}

A general solution for functional languages to convert recursive function to a tail-call equivalent: 函数式语言的一般解决方案,用于将递归函数转换为尾部调用等效函数:

A simple way is to wrap the non tail-recursive function in the Trampoline monad. 一种简单的方法是将非尾递归函数包装在Trampoline monad中。

def pascalM(c: Int, r: Int): Trampoline[Int] = {
 if (c == 0 || c == r) Trampoline.done(1)
 else for {
     a <- Trampoline.suspend(pascal(c - 1, r - 1))
     b <- Trampoline.suspend(pascal(c, r - 1))
   } yield a + b
}

val pascal = pascalM(10, 5).run

So the pascal function is not a recursive function anymore. 所以pascal函数不再是递归函数。 However, the Trampoline monad is a nested structure of the computation that need to be done. 但是,Trampoline monad是一个需要完成计算的嵌套结构。 Finally, run is a tail-recursive function that walks through the tree-like structure, interpreting it, and finally at the base case returns the value. 最后, run是一个尾递归函数,它遍历树状结构,解释它,最后在基本情况下返回值。

A paper from Rúnar Bjanarson on the subject of Trampolines: Stackless Scala With Free Monads 来自RúnarBjanarson的关于蹦床的文章: Stackless Scala with Free Monads

In cases where there is a simple modification to the value of a recursive call, that operation can be moved to the front of the recursive function. 如果对递归调用的值进行简单修改,则可以将该操作移动到递归函数的前面。 The classic example of this is Tail recursion modulo cons , where a simple recursive function in this form: 这个经典的例子是Tail recursion modulo cons ,这里有一个简单的递归函数:

def recur[A](...):List[A] = {
  ...
  x :: recur(...)
}

which is not tail recursive, is transformed into 这不是尾递归,而是转化为

def recur[A]{...): List[A] = {
   def consRecur(..., consA: A): List[A] = {
     consA :: ...
     ...
     consrecur(..., ...)
   }
   ...
   consrecur(...,...)
}

Alexlv's example is a variant of this. Alexlv的例子就是这个的变种。

This is such a well known situation that some compilers (I know of Prolog and Scheme examples but Scalac does not do this) can detect simple cases and perform this optimisation automatically. 这是一个众所周知的情况,一些编译器(我知道Prolog和Scheme示例但Scalac不这样做)可以检测简单情况并自动执行此优化。

Problems combining multiple calls to recursive functions have no such simple solution. 将多个调用组合到递归函数的问题没有这样简单的解决方案。 TMRC optimisatin is useless, as you are simply moving the first recursive call to another non-tail position. TMRC optimisatin是无用的,因为您只是将第一个递归调用移动到另一个非尾部位置。 The only way to reach a tail-recursive solution is remove all but one of the recursive calls; 达到尾递归解决方案的唯一方法是删除除递归调用之外的所有调用; how to do this is entirely context dependent but requires finding an entirely different approach to solving the problem. 如何做到完全取决于上下文,但需要找到一种完全不同的方法来解决问题。

As it happens, in some ways your example is similar to the classic Fibonnaci sequence problem; 碰巧,在某些方面,你的例子类似于经典的Fibonnaci序列问题; in that case the naive but elegant doubly-recursive solution can be replaced by one which loops forward from the 0th number. 在这种情况下,天真但优雅的双递归解决方案可以替换为从第0个数字向前循环的解决方案。

def fib (n: Long): Long = n match {
  case 0 | 1 => n
  case _ => fib( n - 2) + fib( n - 1 )
}

def fib (n: Long): Long = {
  def loop(current: Long, next: => Long, iteration: Long): Long = {
    if (n == iteration) 
      current
    else
      loop(next, current + next, iteration + 1)
  }
  loop(0, 1, 0)
}

For the Fibonnaci sequence, this is the most efficient approach (a streams based solution is just a different expression of this solution that can cache results for subsequent calls). 对于Fibonnaci序列,这是最有效的方法(基于流的解决方案只是该解决方案的不同表达,可以缓存后续调用的结果)。 Now, you can also solve your problem by looping forward from c0/r0 (well, c0/r2) and calculating each row in sequence - the difference being that you need to cache the entire previous row. 现在,您也可以通过从c0 / r0(井,c0 / r2)向前循环并按顺序计算每一行来解决您的问题 - 不同之处在于您需要缓存整个前一行。 So while this has a similarity to fib , it differs dramatically in the specifics and is also significantly less efficient than your original, doubly-recursive solution. 因此虽然这与fib有相似之处,但它在细节方面有很大差异,并且与原始的双递归解决方案相比效率也大大降低。

Here's an approach for your pascal triangle example which can calculate pascal(30,60) efficiently: 这是一个pascal三角形示例的方法,它可以有效地计算pascal(30,60)

def pascal(column: Long, row: Long):Long = {
  type Point = (Long, Long)
  type Points = List[Point]
  type Triangle = Map[Point,Long]
  def above(p: Point) = (p._1, p._2 - 1)
  def aboveLeft(p: Point) = (p._1 - 1, p._2 - 1)
  def find(ps: Points, t: Triangle): Long = ps match {
    // Found the ultimate goal
    case (p :: Nil) if t contains p => t(p)
    // Found an intermediate point: pop the stack and carry on
    case (p :: rest) if t contains p => find(rest, t)
    // Hit a triangle edge, add it to the triangle
    case ((c, r) :: _) if (c == 0) || (c == r) => find(ps, t + ((c,r) -> 1))
    // Triangle contains (c - 1, r - 1)...
    case (p :: _) if t contains aboveLeft(p) => if (t contains above(p))
        // And it contains (c, r - 1)!  Add to the triangle
        find(ps, t + (p -> (t(aboveLeft(p)) + t(above(p)))))
      else
        // Does not contain(c, r -1).  So find that
        find(above(p) :: ps, t)
    // If we get here, we don't have (c - 1, r - 1).  Find that.
    case (p :: _) => find(aboveLeft(p) :: ps, t)
  }
  require(column >= 0 && row >= 0 && column <= row)
  (column, row) match {
    case (c, r) if (c == 0) || (c == r) => 1
    case p => find(List(p), Map())
  }
}

It's efficient, but I think it shows how ugly complex recursive solutions can become as you deform them to become tail recursive. 这是有效的,但我认为它显示了复杂的递归解决方案在变形为尾递归时会变得多么丑陋。 At this point, it may be worth moving to a different model entirely. 在这一点上,完全可能值得转移到另一个模型。 Continuations or monadic gymnastics might be better. 延续monadic体操可能会更好。

You want a generic way to transform your function. 您想要一种通用的方式来转换您的功能。 There isn't one. 没有一个。 There are helpful approaches, that's all. 有一些有用的方法,就是这些。

I don't know how theoretical this question is, but a recursive implementation won't be efficient even with tail-recursion. 我不知道这个问题是如何理论的,但即使使用尾递归,递归实现也不会有效。 Try computing pascal(30, 60) , for example. 例如,尝试计算pascal(30, 60) I don't think you'll get a stack overflow, but be prepared to take a long coffee break. 我认为你不会有堆栈溢出,但要准备好长时间休息一下。

Instead, consider using a Stream or memoization : 相反,请考虑使用Streammemoization

val pascal: Stream[Stream[Long]] = 
  (Stream(1L) 
    #:: (Stream from 1 map { i => 
      // compute row i
      (1L 
        #:: (pascal(i-1) // take the previous row
               sliding 2 // and add adjacent values pairwise
               collect { case Stream(a,b) => a + b }).toStream 
        ++ Stream(1L))
    }))

The accumulator approach 累加器方法

  def pascal(c: Int, r: Int): Int = {

    def pascalAcc(acc:Int, leftover: List[(Int, Int)]):Int = {
      if (leftover.isEmpty) acc
      else {
        val (c1, r1) = leftover.head
        // Edge.
        if (c1 == 0 || c1 == r1) pascalAcc(acc + 1, leftover.tail)
        // Safe checks.
        else if (c1 < 0 || r1 < 0 || c1 > r1) pascalAcc(acc, leftover.tail)
        // Add 2 other points to accumulator.
        else pascalAcc(acc, (c1 , r1 - 1) :: ((c1 - 1, r1 - 1) :: leftover.tail ))
      }
    }

    pascalAcc(0, List ((c,r) ))
  }

It does not overflow the stack but as on big row and column but Aaron mentioned it's not fast. 它没有溢出堆栈,但是在大行和列上,但Aaron提到它并不快。

Yes it's possible. 是的,这是可能的。 Usually it's done with accumulator pattern through some internally defined function, which has one additional argument with so called accumulator logic, example with counting length of a list. 通常它是通过一些内部定义的函数用累加器模式完成的,它有一个额外的参数,所谓的累加器逻辑,例如列表的计数长度。

For example normal recursive version would look like this: 例如,正常的递归版本看起来像这样:

def length[A](xs: List[A]): Int = if (xs.isEmpty) 0 else 1 + length(xs.tail)

that's not a tail recursive version, in order to eliminate last addition operation we have to accumulate values while somehow, for example with accumulator pattern: 这不是尾递归版本,为了消除最后的加法运算,我们必须以某种方式累积值,例如使用累加器模式:

def length[A](xs: List[A]) = {
  def inner(ys: List[A], acc: Int): Int = {
    if (ys.isEmpty) acc else inner(ys.tail, acc + 1)
  }
  inner(xs, 0)
}

a bit longer to code, but i think the idea i clear. 代码要长一点,但我认为这个想法很明确。 Of cause you can do it without inner function, but in such case you should provide acc initial value manually. 因为你可以在没有内部功能的情况下完成它,但在这种情况下你应该手动提供acc初始值。

I'm pretty sure it's not possible in the simple way you're looking for the general case, but it would depend on how elaborate you permit the changes to be. 我敢肯定,这是不可能的,你正在寻找的一般的情况下简单的方法,但是这将取决于你如何精心允许使更改。

A tail-recursive function must be re-writable as a while-loop, but try implementing for example a Fractal Tree using while-loops. 尾递归函数必须可重写为while循环,但尝试使用while循环实现例如Fractal Tree It's possble, but you need to use an array or collection to store the state for each point, which susbstitutes for the data otherwise stored in the call-stack. 这是可能的,但你需要使用一个数组或集合来存储每个点的状态,这些数据会为存储在调用堆栈中的数据提供支持。

It's also possible to use trampolining . 也可以使用蹦床

It is indeed possible. 这确实是可能的。 The way I'd do this is to begin with List(1) and keep recursing till you get to the row you want. 我这样做的方法是从List(1)开始并继续递归,直到你到达你想要的行。 Worth noticing that you can optimize it: if c==0 or c==r the value is one, and to calculate let's say column 3 of the 100th row you still only need to calculate the first three elements of the previous rows. 值得注意的是你可以优化它:如果c == 0或c == r,则值为1,并且要计算第100行的第3列,您仍然只需要计算前面行的前三个元素。 A working tail recursive solution would be this: 一个工作尾递归解决方案是这样的:

def pascal(c: Int, r: Int): Int = {
  @tailrec
  def pascalAcc(c: Int, r: Int, acc: List[Int]): List[Int] = {
    if (r == 0) acc
    else pascalAcc(c, r - 1,
    // from let's say 1 3 3 1 builds 0 1 3 3 1 0 , takes only the
    // subset that matters (if asking for col c, no cols after c are
    // used) and uses sliding to build (0 1) (1 3) (3 3) etc.
      (0 +: acc :+ 0).take(c + 2)
         .sliding(2, 1).map { x => x.reduce(_ + _) }.toList)
  }
  if (c == 0 || c == r) 1
  else pascalAcc(c, r, List(1))(c)
}

The annotation @tailrec actually makes the compiler check the function is actually tail recursive. 注释@tailrec实际上使编译器检查函数实际上是尾递归。 It could be probably be further optimized since given that the rows are symmetric, if c > r/2, pascal(c,r) == pascal ( rc,r).. but left to the reader ;) 它可能可以进一步优化,因为假设行是对称的,如果c> r / 2,pascal(c,r)== pascal(rc,r)..但留给读者;)

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

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