【问题标题】:Merge sort from "Programming Scala" causes stack overflow来自“Programming Scala”的合并排序导致堆栈溢出
【发布时间】:2011-01-13 04:11:55
【问题描述】:

以下算法的直接剪切和粘贴:

def msort[T](less: (T, T) => Boolean)
            (xs: List[T]): List[T] = {
  def merge(xs: List[T], ys: List[T]): List[T] =
    (xs, ys) match {
      case (Nil, _) => ys
      case (_, Nil) => xs
      case (x :: xs1, y :: ys1) =>
        if (less(x, y)) x :: merge(xs1, ys)
        else y :: merge(xs, ys1)
    }
  val n = xs.length / 2
  if (n == 0) xs
  else {
    val (ys, zs) = xs splitAt n
     merge(msort(less)(ys), msort(less)(zs))
  }
}

在 5000 个长列表上导致 StackOverflowError。

有什么办法可以优化这个,这样就不会发生了?

【问题讨论】:

    标签: scala recursion stack-overflow


    【解决方案1】:

    只是玩弄 scala 的TailCalls(蹦床支持),我怀疑最初提出这个问题时它并不存在。这是Rex's answer 中合并的递归不可变版本。

    import scala.util.control.TailCalls._
    
    def merge[T <% Ordered[T]](x:List[T],y:List[T]):List[T] = {
    
      def build(s:List[T],a:List[T],b:List[T]):TailRec[List[T]] = {
        if (a.isEmpty) {
          done(b.reverse ::: s)
        } else if (b.isEmpty) {
          done(a.reverse ::: s)
        } else if (a.head<b.head) {
          tailcall(build(a.head::s,a.tail,b))
        } else {
          tailcall(build(b.head::s,a,b.tail))
        }
      }
    
      build(List(),x,y).result.reverse
    }
    

    在 64 位 OpenJDK(i7 上的 Debian/Squeeze amd64)上的 Scala 2.9.1 上的大 List[Long]s 上的可变版本运行速度一样快。

    【讨论】:

      【解决方案2】:

      以防万一丹尼尔的解决方案不够清楚,问题是合并的递归与列表的长度一样深,而且它不是尾递归,因此无法转换为迭代。

      Scala 可以将 Daniel 的尾递归合并解决方案转换为与此大致等价的东西:

      def merge(xs: List[T], ys: List[T]): List[T] = {
        var acc:List[T] = Nil
        var decx = xs
        var decy = ys
        while (!decx.isEmpty || !decy.isEmpty) {
          (decx, decy) match { 
            case (Nil, _) => { acc = decy.reverse ::: acc ; decy = Nil }
            case (_, Nil) => { acc = decx.reverse ::: acc ; decx = Nil }
            case (x :: xs1, y :: ys1) => 
              if (less(x, y)) { acc = x :: acc ; decx = xs1 }
              else { acc = y :: acc ; decy = ys1 }
          }
        }
        acc.reverse
      }
      

      但它会为您跟踪所有变量。

      (尾递归方法是一种方法调用自己以获得完整的答案并传回;它从不调用自己,然后在将结果传回之前对结果做一些事情。还有, 如果方法可能是多态的,则不能使用尾递归,因此它通常仅适用于对象或标记为 final 的类。)

      【讨论】:

      • 最后一个 acc 实际上应该是 acc.reverse 吗?如果您将其用作独立的合并功能,则应该有,但也许我不明白 msort 的用法。
      【解决方案3】:

      这样做是因为它不是尾递归的。您可以通过使用非严格集合或将其设为尾递归来解决此问题。

      后一种解决方案是这样的:

      def msort[T](less: (T, T) => Boolean) 
                  (xs: List[T]): List[T] = { 
        def merge(xs: List[T], ys: List[T], acc: List[T]): List[T] = 
          (xs, ys) match { 
            case (Nil, _) => ys.reverse ::: acc 
            case (_, Nil) => xs.reverse ::: acc
            case (x :: xs1, y :: ys1) => 
              if (less(x, y)) merge(xs1, ys, x :: acc) 
              else merge(xs, ys1, y :: acc) 
          } 
        val n = xs.length / 2 
        if (n == 0) xs 
        else { 
          val (ys, zs) = xs splitAt n 
          merge(msort(less)(ys), msort(less)(zs), Nil).reverse
        } 
      } 
      

      使用非严格性涉及按名称传递参数,或使用非严格集合,例如Stream。以下代码使用Stream 只是为了防止堆栈溢出,而在其他地方使用List

      def msort[T](less: (T, T) => Boolean) 
                  (xs: List[T]): List[T] = { 
        def merge(left: List[T], right: List[T]): Stream[T] = (left, right) match {
          case (x :: xs, y :: ys) if less(x, y) => Stream.cons(x, merge(xs, right))
          case (x :: xs, y :: ys) => Stream.cons(y, merge(left, ys))
          case _ => if (left.isEmpty) right.toStream else left.toStream
        }
        val n = xs.length / 2 
        if (n == 0) xs 
        else { 
          val (ys, zs) = xs splitAt n 
          merge(msort(less)(ys), msort(less)(zs)).toList
        } 
      }
      

      【讨论】:

      • 我想过尝试让它尾递归,然后看到很多信息声称 JVM 不是那么适合并且并不总是优化尾递归。是否有某种指导方针何时成功?
      • JVM 不会,因此 Scala 编译器会为您完成。它只在一定的条件下才会:必须是自递归(即f调用g,g调用f不行),当然必须是tail递归(递归调用必须始终是该代码路径上的最后一件事),在方法上它必须是 finalprivate。在示例中,因为merge 是在msort 内部定义的,所以它实际上是私有的,而不是在类或对象上定义。
      • 我认为在这里可能值得一提的是 msort 本身不是尾递归,但 merge 是。对于只被编译器说服的任何人,将@tailrec 添加到合并的定义中,您会注意到它被接受为尾递归函数,正如丹尼尔所概述的那样。
      • 现在,话虽如此,同样重要的是要注意 msort 不是尾递归不会成为一个真正的问题。它只会递归 log2(n) 层深度,其中 n 是列表中传入的元素数。因此,对于 5000 个元素的列表,msort 只会递归 13 层深度。
      • @Wilfred merge 不能是尾递归,因为这不是尾调用:x :: merge(xs1, ys)。在tailrec 代码中发现了一个错误,但它看起来不应该在这里应用,所以我很奇怪tailrec 没有抱怨。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-26
      • 2021-01-19
      • 2015-05-21
      • 2014-02-14
      • 1970-01-01
      相关资源
      最近更新 更多