21

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

例如(斯卡拉):

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

函数式语言将递归函数转换为尾调用等效项的通用解决方案:

一种简单的方法是将非尾递归函数包装在Trampolinemonad 中。

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únar Bjanarson 关于蹦床主题的论文:Stackless Scala With Free Monads

4

6 回答 6

26

在对递归调用的值进行简单修改的​​情况下,可以将该操作移至递归函数的前面。典型的例子是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 没有用,因为您只是将第一个递归调用移动到另一个非尾位置。达到尾递归解决方案的唯一方法是删除除一个递归调用之外的所有递归调用;如何做到这一点完全取决于上下文,但需要找到一种完全不同的方法来解决问题。

碰巧,您的示例在某些方面类似于经典的斐波那契数列问题;在这种情况下,可以用从第 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(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())
  }
}

它很有效,但我认为它显示了当您将复杂递归解决方案变形为尾递归时,它们会变得多么丑陋。在这一点上,可能值得完全转向不同的模型。 延续一元体操可能会更好。

您想要一种通用的方法来转换您的功能。没有一个。有一些有用的方法,仅此而已。

于 2013-09-23T07:30:49.160 回答
9

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

相反,请考虑使用 aStreammemoization

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))
    }))
于 2013-09-22T21:46:49.100 回答
5

累加器方法

  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 提到它并不快。

于 2015-10-22T00:23:58.510 回答
4

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

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

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手动提供初始值。

于 2013-09-22T15:09:46.657 回答
3

我很确定以您正在寻找一般情况的简单方式是不可能的,但这取决于您允许更改的详细程度。

尾递归函数必须可重写为 while 循环,但请尝试使用 while 循环实现例如分形树。这是可能的,但是您需要使用数组或集合来存储每个点的状态,以替代存储在调用堆栈中的数据。

也可以使用蹦床

于 2013-09-22T17:03:06.093 回答
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 > r/2,pascal(c,r) == pascal (rc,r).. 但留给读者;)

于 2015-09-24T13:50:00.287 回答