将正常递归转换为尾递归

时间:2013-09-22 14:56:04

标签: scala recursion tail-recursion pascals-triangle

我想知道是否有一些通用的方法来转换带有foo(...) + foo(...)的“正常”递归作为尾递归的最后一次调用。

例如(scala):

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

函数式语言的一般解决方案,用于将递归函数转换为尾部调用等价函数:

一种简单的方法是将非尾递归函数包装在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

因此pascal函数不再是递归函数。但是,Trampoline monad是一个需要完成计算的嵌套结构。最后,run是一个尾递归函数,它遍历树状结构,解释它,最后在基本情况下返回值。

RúnarBjanarson关于蹦床主题的论文:Stackless Scala With Free Monads

6 个答案:

答案 0 :(得分:24)

如果对递归调用的值进行简单修改,则可以将该操作移动到递归函数的前面。这个经典的例子是 Tail recursion modulo cons ,这里有一个简单的递归函数:

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

这不是尾递归,转换为

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

Alexlv的例子就是这个的变种。

这是一个众所周知的情况,一些编译器(我知道Prolog和Scheme示例,但Scalac不这样做)可以检测简单情况并自动执行此优化。

将多个调用组合到递归函数的问题没有这样简单的解决方案。 TMRC optimisatin是无用的,因为您只是将第一个递归调用移动到另一个非尾部位置。达到尾递归解决方案的唯一方法是删除除递归调用之外的所有调用;如何做到完全取决于上下文,但需要找到一种完全不同的方法来解决问题。

碰巧,在某些方面,你的例子类似于经典的Fibonnaci序列问题;在这种情况下,天真但优雅的双递归解决方案可以替换为从第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)
}

对于Fibonnaci序列,这是最有效的方法(基于流的解决方案只是此解决方案的一个不同表达式,可以缓存后续调用的结果)。现在, 你也可以通过从c0 / r0(井,c0 / r2)向前循环并按顺序计算每一行来解决你的问题 - 不同之处在于你需要缓存整个前一行。因此虽然这与 fib 有相似之处,但它在细节方面有很大差异,并且效率也明显低于原始的双递归解决方案。

以下是您的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())
  }
}

它很有效,但我认为它显示了复杂的递归解决方案在你变形成尾部递归时会变得多么丑陋。在这一点上,完全可能值得转移到另一个模型。 Continuationsmonadic gymnastics可能会更好。

您想要一种通用的方式来转换您的功能。没有一个。有一些有用的方法,就是这些。

答案 1 :(得分:9)

我不知道这个问题是如何理论的,但即使使用尾递归,递归实现也不会有效。例如,尝试计算pascal(30, 60)。我不认为你会得到一个堆栈溢出,但准备好喝咖啡休息时间。

相反,请考虑使用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))
    }))

答案 2 :(得分:4)

累加器方法

  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) ))
  }

它没有溢出堆栈,但是在大行和列上,但Aaron提到它并不快。

答案 3 :(得分:3)

是的,这是可能的。通常它是通过一些内部定义的函数用累加器模式完成的,它有一个额外的参数,所谓的累加器逻辑,例如列表的计数长度。

例如,正常的递归版本如下所示:

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

这不是尾递归版本,为了消除最后的加法运算,我们必须以某种方式累积值,例如使用累加器模式:

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)
}

编码时间稍长,但我认为这个想法很明确。因为你可以在没有内部函数的情况下完成它,但在这种情况下你应该手动提供acc初始值。

答案 4 :(得分:3)

我很确定你可以通过简单的方式找到一般情况 ,但这取决于你允许更改的复杂程度。

尾递归函数必须可以作为while循环重写,但尝试使用while循环实现例如Fractal Tree。这是可能的,但你需要使用一个数组或集合来存储每个点的状态,这些数据会为存储在调用堆栈中的数据提供支持。

也可以使用trampolining

答案 5 :(得分:2)

确实有可能。我这样做的方法是 从List(1)开始并继续递归直到你到达 你想要的一排。 值得注意的是你可以优化它:如果c == 0或c == r,则值为1,并且要计算第100行的第3列,您仍然只需要计算前面行的前三个元素。 一个工作尾递归解决方案是:

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)
}

注释 @tailrec 实际上使编译器检查该函数 实际上是尾递归。 如果c> 1,则可以进一步优化,因为行是对称的。 r / 2,pascal(c,r)== pascal(r-c,r)..但留给读者;)