当前位置: 首页 > 知识库问答 >
问题:

将正常递归转换为尾部递归

壤驷穆冉
2023-03-14

我想知道是否有一些通用方法可以用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)
}

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

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

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函数不再是递归函数。然而,蹦床单子是需要完成的计算的嵌套结构。最后,run是一个尾递归函数,它遍历树状结构,解释它,最后在基本情况下返回值。

Rúnar Bjanarson关于蹦床主题的论文:带自由单子的无堆栈Scala

共有3个答案

娄飞鸾
2023-03-14

累加器方法

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

它不会溢出堆栈,但在大行和列,但亚伦提到它的速度不快。

戚正业
2023-03-14

我不知道这个问题有多理论,但递归实现即使使用尾部递归也不会有效。例如,尝试计算帕斯卡(30,60)。我不认为你的咖啡堆会溢出来,但要做好准备,好好Rest一下。

相反,请考虑使用Stream或备忘录:

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))
    }))
秦宜修
2023-03-14

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

碰巧的是,在某些方面,您的示例类似于经典的斐波那契序列问题;在这种情况下,简单但优雅的双递归解决方案可以替换为从第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有相似之处,但它在细节上有很大不同,而且效率也明显低于您最初的双递归解决方案。

下面是帕斯卡三角形示例的一种方法,可以有效地计算帕斯卡(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())
  }
}

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

你想要一种通用的方法来转换你的函数。没有。只有有用的方法,仅此而已。

 类似资料:
  • 我有一个关于如何将“递归”转换为“尾部递归”的问题。 这不是家庭作业,只是在我试图润色算法书籍中的递归定理时出现的一个问题。 我熟悉使用递归的两个典型示例(阶乘和斐波那契序列),也知道如何以递归方式和尾部递归方式实现它们。 我的代码如下(我使用Perl只是为了使其简单,但可以轻松地转换为C/Java/C)。 运行代码时,输出如下: 递归函数在返回之前使用不同的参数调用自己两次。我尝试了几种方法将其

  • 我试图了解如何将各种递归函数转换为尾递归。我已经查看了许多将斐波那契和阶乘转换为尾递归的示例,并理解了这些示例,但很难跳到具有某种不同结构的问题。一个例子是: 如何将其转换为尾部递归实现? 我已经看过类似的问题,例如:将正常递归转换为尾部递归,但这些似乎并没有转化为这个问题。

  • 我只是想知道这样的函数是否可以递归地完成。我觉得很难,因为它自己叫了两次。 这是我在javascript中的非尾部递归实现。(是的,我知道大多数javascript引擎不支持TCO,但这只是理论上的。)目标是找到给定数组(arr)中具有特定长度(大小)的所有子列表。例如:getSublistsWithFixedSize([1,2,3],2)返回[[1,2]、[1,3]、[2,3]]

  • 我们正在获取具有以下字段的订单数据(仅显示相关字段) 具有NULLoriginal_orderid的订单可以被认为是父订单 其中一些父母订单可能有子订单,子订单的original_orderid映射到父母的订单。 子顺序可以产生另一个子顺序,如图像所示,带有颜色编码。 与原始文本相同的数据: 作为转换,我们需要将所有子节点映射到它们的原始父节点(original_orderid为NULL),并获得

  • 我对函数式编程很陌生,尤其是下面使用的Scheme。我正在尝试使以下函数是递归的,尾递归的。基本上,该函数的作用是对两个字符串的对齐方式进行评分。当给定两个字符串作为输入时,它会比较每个“列”字符,并根据在称为 scorer 的函数中实现的评分方案(由下面的代码中的函数调用)来累积该对齐的分数。 我有一个想法,用一个帮助函数来累积分数,但我不太确定如何去做,因此我该如何让下面的函数尾递归呢?

  • 尾递归优化 recur 尾递归优化是函数式编程不能缺少的一个性能优化方案. 没有尾递归, 常有的递归调用也会形成很深的调用栈消耗内存. cljs 和 Clojure 类似, 都需要通过声明 recur 进行优化. 最终代码会被编译为 white 结构的 js 代码,从而提高性能. (defn factorial [acc n] (if (<= n 1) acc (recur (* ac