快速功能合并排序

时间:2013-09-22 13:47:38

标签: algorithm scala sorting

这是我在Scala中实现合并排序的方法:

object FuncSort {
  def merge(l: Stream[Int], r: Stream[Int]) : Stream[Int] = {
    (l, r) match {
      case (h #:: t, Empty) => l
      case (Empty, h #:: t) => r
      case (x #:: xs, y #:: ys) => if(x < y ) x #:: merge(xs, r) else y #:: merge(l, ys)
    }
  }

  def sort(xs: Stream[Int]) : Stream[Int] = {
    if(xs.length == 1) xs
    else {
      val m = xs.length / 2
      val (l, r) = xs.splitAt(m)
      merge(sort(l), sort(r))
    }
  }
}

它工作正常,似乎渐近它也很好但是它比这里http://algs4.cs.princeton.edu/22mergesort/Merge.java.html的Java实现慢得多(大约10倍)并且使用了大量内存。是否有更快的实现合并排序功能?显然,可以逐行移植Java版本,但这不是我想要的。

UPD:我已将Stream更改为List并将#::更改为::,并且排序例程变得更快,仅比Java版本慢三到四倍。但是我不明白为什么它不会因堆栈溢出而崩溃? merge不是尾递归的,所有参数都经过严格评估......怎么可能?

3 个答案:

答案 0 :(得分:3)

您提出了多个问题。我尝试按逻辑顺序回答它们:

Stream版本中没有堆栈溢出

你并没有真正问过这个问题,但它引出了一些有趣的观察结果。

在Stream版本中,您在 #:: merge(...)函数中使用merge。通常这将是一个递归调用,并可能导致堆栈溢出,以获得足够大的输入数据。但不是在这种情况下。运算符#::(a,b)class ConsWrapper[A]中实现(存在隐式转换),并且是cons.apply[A](hd: A, tl: ⇒ Stream[A]): Cons[A]的同义词。正如您所看到的,第二个参数是按名称调用,这意味着它会被懒惰地评估。

这意味着merge返回一个新创建的cons类型的对象,它最终会再次调用merge。换句话说:递归不会发生在堆栈上,而是发生在堆上。通常你有足够的堆。

使用堆进行递归是一种处理非常深度递归的好方法。但它比使用堆栈慢得多。所以你以递归深度交换速度。这是主要原因,为什么使用Stream这么慢。

第二个原因是,为了获得Stream的长度,Scala必须实现整个Stream。但是在排序期间Stream无论如何都必须实现每个元素,所以这不会造成太大的伤害。

列表版本

中没有堆栈溢出

当您更改Stream for List时,您确实使用堆栈进行递归。现在可能发生堆栈溢出。但是通过排序,您的递归深度通常为log(size),通常是基数2的对数。因此,要对40亿个输入项进行排序,您需要大约32个堆栈帧。默认堆栈大小至少为320k(在Windows上,其他系统具有较大的默认值),这会留下很多递归,因此需要对大量输入数据进行排序。

更快的功能实施

取决于: - )

您应该使用堆栈,而不是堆来递归。您应该根据输入数据确定您的策略:

  1. 对于小数据块,使用一些直接算法对它们进行排序。算法的复杂性不会让你感到困惑,并且你可以通过在缓存中拥有所有数据来获得很多性能。当然,你仍然可以为给定的大小提供代码sorting networks
  2. 如果您有数字输入数据,则可以使用radix sort并处理处理器或GPU上的矢量单位的工作(可以在GPU Gems中找到更复杂的算法)。
  3. 对于中等大小的数据块,您可以使用分而治之策略将数据拆分为多个线程(仅当您有多个核心时!)
  4. 对于大型数据块,使用合并排序并将其拆分为适合内存的块。如果需要,可以在网络上分发这些块并在内存中进行排序。
  5. 不要使用swap并使用您的缓存。如果可以,请使用可变数据结构并进行排序。我认为功能和快速排序不能很好地协同工作。要快速排序,您必须使用有状态操作(例如,对可变数组进行就地合并)。

    我通常在我的所有程序上尝试这个:尽可能使用纯函数样式,但在可行的情况下对小部件使用有状态操作(例如,因为它具有更好的性能或代码只需处理大量状态并变得更多当我使用var而不是val时,可以更好地阅读。

答案 1 :(得分:2)

这里有几点需要注意。

首先,您没有正确考虑初始流的排序为空的情况。您可以通过修改排序中的初始检查来解决此问题,以阅读if(xs.length <= 1) xs

其次,流可能具有不可计算的长度(例如Strem.from(1)),这在尝试计算其中一半(可能无限长)时会产生问题 - 您可能需要考虑使用{{ 1}}或类似的(尽管天真地使用它可以过滤掉一些可以计算的流)。

最后,定义为在流上运行的事实可能会减慢它的速度。我尝试对您的流版本的mergesort进行大量运行,而不是编写到进程列表的版本,并且列表版本的出现速度提高了大约3倍(诚然,只有一对运行)。这表明以这种方式处理流的效率低于列表或其他序列类型(Vector可能更快,或者根据引用的Java解决方案使用数组)。

那就是说,我不是时间和效率方面的优秀专家,所以其他人也许能够做出更明智的回应。

答案 2 :(得分:0)

您的实施是自上而下的合并排序。我发现自下而上的合并排序更快,与List.sorted相当(对于我的测试用例,随机大小的随机数列表)。

def bottomUpMergeSort[A](la: List[A])(implicit ord: Ordering[A]): List[A] = {
  val l = la.length

  @scala.annotation.tailrec
  def merge(l: List[A], r: List[A], acc: List[A] = Nil): List[A] = (l, r) match {
    case (Nil, Nil)           => acc
    case (Nil, h :: t)        => merge(Nil, t, h :: acc)
    case (h :: t, Nil)        => merge(t, Nil, h :: acc)
    case (lh :: lt, rh :: rt) =>
      if(ord.lt(lh, rh)) merge(lt, r, lh :: acc)
      else               merge(l, rt, rh :: acc)
  }

  @scala.annotation.tailrec
  def process(la: List[A], h: Int, acc: List[A] = Nil): List[A] = {
    if(la == Nil) acc.reverse
    else {
      val (l1, r1) = la.splitAt(h)
      val (l2, r2) = r1.splitAt(h)

      process(r2, h, merge(l1, l2, acc))
    }
  }

  @scala.annotation.tailrec
  def run(la: List[A], h: Int): List[A] =
    if(h >= l) la
    else       run(process(la, h), h * 2)

  run(la, 1)
}