【问题标题】:Reverse list Scala反向列表Scala
【发布时间】:2011-11-12 19:48:04
【问题描述】:

给定以下代码:

import scala.util.Random

object Reverser {

  // Fails for big list
  def reverseList[A](list : List[A]) : List[A] = {
    list match {
      case Nil => list
      case (x :: xs) => reverseList(xs) ::: List(x)
    }
  }

  // Works
  def reverseList2[A](list : List[A]) : List[A] = {
    def rlRec[A](result : List[A], list : List[A]) : List[A] = {
      list match {
        case Nil => result
        case (x :: xs) => { rlRec(x :: result, xs) }
      }
    }
    rlRec(Nil, list)
  }

  def main(args : Array[String]) : Unit = {
    val random = new Random
    val testList = (for (_ <- 1 to 2000000) yield (random.nextInt)).toList
    // val testListRev = reverseList(testList) <--- Fails
    val testListRev = reverseList2(testList)
    println(testList.head)
    println(testListRev.last)
  }
}

为什么函数的第一个版本失败(对于大输入),而第二个变体有效。我怀疑这与尾递归有关,但我不太确定。有人可以给我“傻瓜”的解释吗?

【问题讨论】:

  • 为什么不使用'val testListRev = testList.reverse'?
  • 这个问题是很久以前提出的,但这里是你的尾递归问题的答案。是的,这是因为尾递归优化。在您的第一个实现中,case (x :: xs) => reverseList(xs) ::: List(x),在递归调用 reverseList 之后,程序必须将 List(x) 添加到其中。这意味着它不能被优化成一个循环,在你的第二个例子中: case (x :: xs) => { rlRec(x :: result, xs) } rlRec 是最后一次调用,退出后无事可做,这就是为什么它不必为它创建一个新的堆栈框架。

标签: list scala recursion functional-programming tail-recursion


【解决方案1】:

第一种方法不是尾递归。见:

case (x :: xs) => reverseList(xs) ::: List(x)

最后调用的操作是:::,而不是递归调用reverseList。另一种是尾递归。

【讨论】:

    【解决方案2】:

    函数被称为tail recursive,当它调用自己作为它的最后一个动作时。您可以通过添加@tailrec注解来检查函数是否为tail recursive

    【讨论】:

      【解决方案3】:

      好吧,让我尝试一下假人的尾递归

      如果你遵循必须用 reverseList 做的事情,你会得到

      reverseList(List(1,2,3, 4))
      reverseList(List(2,3,4):::List(1)
      (reverseList(List(3,4):::List(2)):::List(1)   
      ((reverseList(List(4):::List(3)):::List(2)):::List(1)
      Nil:::List(4):::List(3):::List(2):::List(1)
      List(4,3,2,1)
      

      有了 rlRec,你就有了

      rlRec(List(1,2,3,4), Nil)
      rlRec(List(2,3,4), List(1))
      rlREc(List(3,4), List(2,1))
      rlRec(List(4), List(3,2,1))
      rlRec(Nil, List(4,3,2,1))
      List(4,3,2,1)
      

      不同之处在于,在第一种情况下,重写会越来越长。您必须记住在对reverseList 的最后一次递归调用完成后要做的事情:要添加到结果中的元素。堆栈用于记住这一点。当这太过分时,您会遇到堆栈溢出。相反,使用rlRec,重写始终具有相同的大小。当最后一个 rlRec 完成时,结果可用。没有其他事情可做,无需记住,无需堆栈。关键是在rlRec中,递归调用是return rlRec(something else),而在reverseList中是return f(reverseList(somethingElse))f_ ::: List(x)。你需要记住你必须调用f(这意味着记住x)(在scala中不需要return,只是为了清楚起见而添加。还要注意val a = recursiveCall(x); doSomethingElse()doSomethingElseWith(recursiveCall(x))相同,所以它是不是尾声)

      当你有递归尾调用时

      def f(x1,...., xn)
          ...
          return f(y1, ...yn)
          ...
      

      实际上没有必要记住第一个f 的上下文,以便知道第二个何时返回。所以可以改写

      def f(x1....xn)
      start:
          ...
          x1 = y1, .... xn = yn
          goto start
          ...
      

      这就是编译器所做的,因此您可以避免堆栈溢出。

      当然,函数f 需要在某个不是递归调用的地方返回。这就是goto start 创建的循环将退出的地方,就像递归调用系列停止的地方一样。

      【讨论】:

        【解决方案4】:

        正如其他人所提到的,尾调用消除可避免在不需要时增加堆栈。如果您对优化的作用感到好奇,可以运行

        scalac -Xprint:tailcalls MyFile.scala
        

        ...在消除阶段之后显示编译器中间表示。 (请注意,您可以在任何阶段之后执行此操作,并且可以使用 scala -Xshow-phases 打印阶段列表。)

        例如,对于您的内部尾递归函数 rlRec,它给了我:

        def rlRec[A >: Nothing <: Any](result: List[A], list: List[A]): List[A] = {
          <synthetic> val _$this: $line2.$read.$iw.$iw.type = $iw.this;
          _rlRec(_$this,result,list){
            list match {
              case immutable.this.Nil => result
              case (hd: A, tl: List[A])collection.immutable.::[A]((x @ _), (xs @ _)) => _rlRec($iw.this, {
                <synthetic> val x$1: A = x;
                result.::[A](x$1)
              }, xs)
            }
          }
        }
        

        不管合成的东西,重要的是 _rlRec 是一个标签(即使它看起来像一个函数),并且模式匹配的第二个分支中对 _rlRec 的“调用”将被编译为跳转在字节码中。

        【讨论】:

          【解决方案5】:

          您可以通过使用默认参数为结果提供初始值,使尾递归版本与非尾递归版本一样简单:

          def reverseList[A](list : List[A], result: List[A] = Nil) : List[A] = list match {
            case Nil => result
            case (x :: xs) => reverseList(xs, x :: result)
          }
          

          虽然您可以像其他方式一样使用它,即reverseList(List(1,2,3,4)),但不幸的是,您使用可选的result 参数公开了实现细节。目前似乎没有办法隐藏它。这可能会或可能不会让您担心。

          【讨论】:

          • Scala List 类有一个名为reverse_::: 的方法,它几乎完全可以做到这一点。文档描述了它的作用:“在此列表前面以相反的顺序添加给定列表的元素”。突然间,那个“额外”的论点是一个特征!我们可以通过someList reverse_::: Nil 来反转它,或者someList reverse_::: otherListsomeList 反转到otherList 的前面。通常情况下,通过一点“品牌重塑”,您添加到函数中以支持尾递归(称为累加器)的额外参数实际上概括了您的方法的目的。
          【解决方案6】:
          def reverse(n: List[Int]): List[Int] = {
            var a = n
            var b: List[Int] = List()
            while (a.length != 0) {
              b = a.head :: b
              a = a.tail
            }
            b
          }
          

          当你调用函数时这样调用它,

          reverse(List(1,2,3,4,5,6))
          

          然后它会给出这样的答案,

          res0: List[Int] = List(6, 5, 4, 3, 2, 1)
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2021-12-25
            • 1970-01-01
            • 2011-12-03
            • 2017-11-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-11-11
            相关资源
            最近更新 更多