【问题标题】:Convert normal recursion to tail recursion将普通递归转换为尾递归
【发布时间】:2013-09-27 12:23:47
【问题描述】:

我想知道是否有一些通用方法可以将“正常”递归与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únar Bjanarson 关于蹦床主题的论文:Stackless Scala With Free Monads

【问题讨论】:

  • 您好,很好的问题,但我想指出您对pascal 的第一个递归实现是不完整的。如果您执行pascal(1,0),那么您会得到stackoverflow 异常,因此您可能想要添加条件if(c&lt;0 || r&lt;0 || c&gt;r) throw new IllegalArgumentException("Columns can never be bigger than lines") 或者ArithmeticException

标签: scala recursion tail-recursion pascals-triangle


【解决方案1】:

累加器方法

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

【讨论】:

    【解决方案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 (r-c,r).. 但留给读者;)

    【讨论】:

      【解决方案3】:

      在对递归调用的值进行简单修改的​​情况下,可以将该操作移至递归函数的前面。这方面的经典示例是 尾递归模 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())
        }
      }
      

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

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

      【讨论】:

      • 不错的答案。 Rúnar 的论文提供了特别丰富的信息,尽管它可能与您的最终主张相矛盾(具体取决于您所想到的转换)。他的 Trampoline 转换将产生一个堆栈友好的实现,即使指数运行时仍然是一个问题。
      【解决方案4】:

      我不知道这个问题的理论性如何,但是即使使用尾递归,递归实现也不会有效。例如,尝试计算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))
          }))
      

      【讨论】:

      • 我意识到这并不能直接回答您的问题,但我决定将其作为答案而不是评论发布,因为您可能会遇到任何不重要的效率问题这种形式的重复。
      • 如果我们正在做帕斯卡三角的替代实现,val pascal = Stream.iterate(Seq(1))(a=&gt;(0+:a,a:+0).zipped.map(_+_))
      • @LuigiPlinge 美丽!
      • @AaronNovstrup 非常好的流示例!
      • @LuigiPlinge 我猜你的代码有问题,我得到了负值:pascal(60)(30) = -1515254800
      【解决方案5】:

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

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

      也可以使用trampolining

      【讨论】:

      【解决方案6】:

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

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

      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 初始值。

      【讨论】:

      • 一个主要的区别是在帕斯卡的例子中,你必须递归两次。您可以将第一个结果存储到累加器中,但首先获得它不会是 TCO。如何解决这个问题?
      • @yshavit 无法检查此解决方案,但可能有两个累加器,从 tailrec 内部函数返回元组,然后求和?
      • 我的直觉是通过累加器是不可能的(正如 Luigi 在下面提到的,没有模拟调用堆栈 Ina 局部变量)。
      • @AlexIv 但这并不能成为这个特定问题的答案,您似乎没有仔细阅读。
      • 这是一个带有测试用例的问题,您的答案似乎忽略了它。如果不展示如何解决测试用例,您并没有真正回答问题。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-01-13
      • 2019-09-14
      • 2016-01-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多