【问题标题】:Get results for sub problems using tail call recursion in Scala在 Scala 中使用尾调用递归获取子问题的结果
【发布时间】:2019-11-13 02:24:37
【问题描述】:

我正在尝试使用@tailrec 计算每个子问题的结果 类似于正常递归解决方案如何为每个子问题生成解决方案。 以下是我处理的示例。

@tailrec
  def collatz(
      n: BigInt,
      acc: BigInt,
      fn: (BigInt, BigInt) => Unit
  ): BigInt = {
    fn(n, acc)
    if (n == 1) {
      acc
    } else if (n % 2 == 0) {
      collatz(n / 2, acc + 1, fn)
    } else {
      collatz(3 * n + 1, acc + 1, fn)
    }
  }

这里我使用Collatz Conjecture 计算一个数字达到1 时的计数。举个例子,让我们假设它的号码为32

val n = BigInt("32")
    val c = collatz(n, 0, (num, acc) => {
      println("Num -> " + num + " " + " " + "Acc -> " + acc)
    })

我得到以下输出。

Num -> 32  Acc -> 0
Num -> 16  Acc -> 1
Num -> 8  Acc -> 2
Num -> 4  Acc -> 3
Num -> 2  Acc -> 4
Num -> 1  Acc -> 5

普通递归解决方案将返回每个数字的准确计数。例如编号21 步骤中达到1。因此,每个子问题都有精确的解决方案,但在tailrec 方法中,只有最终结果被正确计算。变量acc 的行为与预期的循环变量完全相同。

如何更改尾调用优化的代码,同时我可以获得每个子问题的准确值。简而言之,我怎样才能获得Stack 变量的acc 行为类型。

另外,一个相关的问题是 lambda 函数 fn 的开销对于 n 的大值会有多大,假设不会使用 println 语句。

我正在添加一个递归解决方案,它可以为子问题产生正确的解决方案。

def collatz2(
      n: BigInt,
      fn: (BigInt, BigInt) => Unit
  ): BigInt = {

    val c: BigInt = if (n == 1) {
      0
    } else if (n % 2 == 0) {
      collatz2(n / 2, fn) + 1
    } else {
      collatz2(3 * n + 1, fn) + 1
    }
    fn(n, c)
    c
  }

它产生以下输出。

Num -> 1  Acc -> 0
Num -> 2  Acc -> 1
Num -> 4  Acc -> 2
Num -> 8  Acc -> 3
Num -> 16  Acc -> 4
Num -> 32  Acc -> 5

【问题讨论】:

  • 它将递归调用变成一个循环。对不起 tldr;一个常见的习惯用法是制作一个本地 def tailrec,其中一些 args,如来自外部 def 的 fn 是固定的。然后 outdef 只是用初始值调用 tailrec f。
  • @som-snytt 我知道我们可以使用嵌套方法来使默认参数更好地工作。我的主要问题是关于如何使尾递归解决方案解决类似于递归解决方案的子问题。
  • 您首先说您想“找到 [the] annotation @tailrec 的工作原理”,但您的问题完全是关于尾递归代码,而不是注释。 @tailrec 注解对编译后的代码没有影响。如果指定的例程不是尾递归,它只会发出错误。
  • @jwvh 在正常的递归函数中,我们可以为基本情况返回 1,递归调用返回子问题的结果。添加@tailrec 注解使其仅使用acc 变量。
  • @Hariharan,这是不正确的。添加@tailrec 不会改变代码的编译或运行方式。它的唯一目的是在编译时通知开发人员,如果注解的方法是不是尾递归的。如果方法尾递归的,那么注解什么也不做。如果方法是 not 尾递归的,那么注解只会停止编译。就是这样。

标签: scala recursion tail-recursion tail-call-optimization


【解决方案1】:

使用尾递归(不使用显式堆栈)时,您无法“获得堆栈类型的行为”。 @tailrec 注释表示您没有使用调用堆栈,并且可以对其进行优化。您必须决定是要尾递归还是递归子问题求解。一些问题(例如二进制搜索)非常适合尾递归,而其他问题(例如您的 collat​​z 代码)需要更多的思考,还有一些问题(例如 DFS)过于依赖调用堆栈而无法从尾递归中受益.

【讨论】:

    【解决方案2】:

    我不确定我是否正确理解了您的问题。听起来您要求我们编写 collat​​z2 以便它是尾递归的。我用两种方式重写了它。

    虽然我提供了两种解决方案,但它们实际上是一回事。一个使用 List 作为堆栈,其中 List 的头部是堆栈的顶部。另一个使用 mutable.Stack 数据结构。研究这两个解决方案,直到您明白为什么它们都与原始问题中的 collat​​z2 相同。

    为了让程序尾递归,我们要做的是模拟将值压入堆栈,然后将它们一个一个弹出的效果。在弹出阶段,我们为 Acc 赋值。 (对于那些不记得的人,Hariharan 的说法中的 Acc 是每个术语的索引。)

    import scala.collection.mutable
    
    object CollatzCount {
    
      def main(args: Array[String]) = {
        val start = 32
    
        collatzFinalList(start, printer)
    
        collatzFinalStack(start, printer)
    
      }
    
      def collatzInnerList(n: Int, acc: List[Int]): List[Int] = {
        if (n == 1) n :: acc
        else if (n % 2 == 0) collatzInnerList(n/2, n :: acc )
        else collatzInnerList(3*n + 1, n :: acc )
      }
    
      def collatzFinalList(n: Int, fun: (Int, Int)=>Unit): Unit = {
        val acc = collatzInnerList(n, List())
        acc.foldLeft(0){ (ctr, e) =>
          fun(e, ctr)
          ctr + 1
        }
      }
    
      def collatzInnerStack(n: Int, stack: mutable.Stack[Int]): mutable.Stack[Int] = {
        if (n == 1) {
          stack.push(n)
          stack
        } else if (n % 2 == 0) {
          stack.push(n)
          collatzInnerStack(n/2, stack)
        } else {
          stack.push(n)
          collatzInnerStack(3*n + 1, stack)
        }
      }
    
      def popStack(ctr: Int, stack: mutable.Stack[Int], fun: (Int, Int)=>Unit): Unit = {
        if (stack.nonEmpty) {
          val popped = stack.pop
          fun(popped, ctr)
          popStack(ctr + 1, stack, fun)
        } else ()
      }
    
    
      def collatzFinalStack(n: Int, fun: (Int, Int) => Unit): Unit = {
        val stack = collatzInnerStack(n, mutable.Stack())
        popStack(0, stack, fun)
      }
    
    
      val printer = (x: Int, y: Int) => println("Num ->" + x + " " + " " + "Acc -> " + y)
    
    }
    

    【讨论】:

    • 堆栈解决方案很好地模拟了调用堆栈,尽管它比大型调用堆栈有很大的改进,对于非常大的 n 值,我们需要将它们全部存储在堆栈中。当前的堆栈解决方案是否可以在不存储所有值的情况下进行重构?作为参考,尾递归解决方案不存储所有值,仅使用 lambda 返回当前值。我认为它会帮助它无限运行。
    • @Hariharan 无法避免存储所有值,因为否则您无法计算正确的 Acc 值。该程序从根本上受到 JVM 堆空间的限制,如果不持久化堆栈的状态,就无法绕过它。持久性解决方案的一个示例是将堆栈的状态存储在数据库中。请意识到没有办法重构程序,使其可以永远运行。最后,无论如何,您都会受到计算机磁盘空间量的限制,即使使用持久性解决方案也是如此。
    • @Hariharan 另外,重复我在答案中所说的话,两个答案实际上都是堆栈解决方案。它们的作用与 collat​​z2 完全相同。
    • 我知道我们不能无限运行。即使n:BigInt 也必须存储在内存中,所以在某一时刻它会失败。
    • 我用Map而不是Stack尝试了您的解决方案,然后我们可以在回调中进行几次迭代后清除未使用的Map。它比原始解决方案更好。我还检查了他们在动态编程中使用这种方法,我们可以对以前计算的值进行记忆。
    猜你喜欢
    • 2016-08-25
    • 1970-01-01
    • 2016-03-21
    • 1970-01-01
    • 1970-01-01
    • 2018-03-17
    • 2018-03-08
    • 1970-01-01
    • 2011-12-10
    相关资源
    最近更新 更多