![](/img/trans.png)
[英]How do you prove or illustrate that fast merge sort is an unstable algorithm?
[英]Fast functional merge sort
这是我在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))
}
}
}
它可以正常工作,并且似乎在渐近上也可以,但是它比Java实现慢得多(大约10倍),可以从此处http://algs4.cs.princeton.edu/22mergesort/Merge.java.html进行,并使用很多内存。 是否有更快的合并排序功能实现 ? 显然,可以逐行移植Java版本,但这不是我想要的。
UPD:我将Stream
更改为List
,将#::
更改为::
,并且排序例程变得更快,仅比Java版本慢三到四倍。 但是我不明白为什么它不会因堆栈溢出而崩溃? merge
不是尾递归的,所有参数都经过严格评估...这怎么可能?
您提出了多个问题。 我尝试按照逻辑顺序回答它们:
您并没有真正问过这个问题,但是它导致了一些有趣的观察。
在Stream版本中,您在merge
功能内使用#:: merge(...)
。 通常,这将是递归调用,并可能导致堆栈溢出,无法容纳足够大的输入数据。 但在这种情况下不是。 运算符#::(a,b)
在class ConsWrapper[A]
实现(存在隐式转换),并且是cons.apply[A](hd: A, tl: ⇒ Stream[A]): Cons[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上,其他系统具有更大的默认值),因此有很多递归空间,因此可以对许多输入数据进行排序。
这取决于 :-)
您应该使用堆栈,而不要使用堆进行递归。 您应该根据输入数据决定策略:
不要使用swap并使用缓存。 如果可以,请使用可变数据结构并进行适当排序。 我认为功能排序和快速排序不能很好地协同工作。 为了使排序真正快速,您将必须使用有状态操作(例如,对可变数组进行就地归并排序)。
我通常在所有程序上都尝试这样做:尽可能使用纯函数样式,但在可行的情况下对小部分使用有状态操作(例如,因为它具有更好的性能,或者代码只需要处理很多状态,并且在处理时会变得更好可读)我使用var
而不是val
)。
这里有几件事要注意。
首先,您没有正确考虑初始流排序为空的情况。 您可以通过修改sort内部的初始检查以读取if(xs.length <= 1) xs
来解决此问题。
其次,流的长度可能无法计算(例如Strem.from(1)
),这在尝试计算该长度(可能是无限长)的一半时会出现问题-您可能需要考虑使用hasDefiniteSize
或类似方法检查该hasDefiniteSize
(尽管天真地使用了它,但可以过滤掉一些原本可以计算的流)。
最后,将其定义为对流进行操作的事实可能会使它变慢。 我尝试对您的mergesort流版本的大量运行与写入进程列表的版本进行定时运行,并且列表版本的发布速度大约快了3倍(只能在一对运行中运行)。 这表明,与列表或其他序列类型相比,以这种方式使用流的效率较低(Vector可能仍然更快,或者按照引用的Java解决方案使用数组)。
就是说,我不是时间和效率方面的优秀专家,因此其他人也许可以给出更容易理解的答复。
您的实现是自上而下的合并排序。 我发现自下而上的合并排序速度更快,并且可以与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)
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.