合并排序例程中有以下片段
val m = xs.length / 2
val (l, r) = xs.splitAt(m)
streamMerge(streamSort(l), streamSort(r))
是否有更功能(和懒惰)的方法将流拆分为两个?我已经尝试从这里http://en.literateprograms.org/Merge_sort_(Haskell)移植拆分例程,但它会导致堆栈溢出崩溃。
答案 0 :(得分:3)
我看到两种可能性:不要使用长度或不使用流。
长度是流的严格函数,所以你不能这样做。但是存在多种非严格的可能性:
从流中获取前三个元素。当少于三个时,首先分裂是没有任何意义的。然后使用Stream.partition(_ > biggestElement)
分割大于这三个元素的第一个元素。
这通常可以很好地工作,但在特殊数据星座上会有问题(例如输入已经排序)。
均匀分割流,但不在中间。使用Stream.zipWithIndex.partition(_._2 %2 == 0)
获取两个流。
如果您将通过网络排序的某些部分卸载到其他节点,这可能是一种很好的方法。
当您不使用流时,您的算法可能运行得更快,但获取大小的数据结构便宜。
如果您使用可变集合,甚至可以sort in place。当你在本地拥有所有数据时(例如在RAM或内存映射文件中),这应该是最快的方法。
答案 1 :(得分:2)
如果你真的想在引用中实现split
,你必须使go
尾递归。
正常实施(或多或少复制):
def go[A](v: (Stream[A], Stream[A])): (Stream[A], Stream[A]) = v match {
case (x #:: xs, _ #:: _ #:: zs) =>
val (us,vs) = go((xs,zs))
(x #:: us, vs)
case (xs, _) => (Stream.empty, xs)
}
这会溢出堆栈。
现在我们只是让它尾递归:
def go[A](v: (Stream[A], Stream[A]), acc: Stream[A]): (Stream[A], Stream[A]) = v match {
case (x #:: xs, _ #:: _ #:: zs) =>
go((xs,zs), x #:: acc)
case (xs, _) => (acc.reverse, xs)
}
现在打电话:
go((x,x), Stream.empty)
你得到一个懒惰的分裂,没有堆栈溢出(测试时我先填满我的记忆)。
正如我的评论所提到的,这最后的解决方案不适用于无限流。在这种情况下的问题是结果的右侧:为了知道结果流(它只是原始的尾部),我们必须完全评估原始流。
允许无限流的实现使这一点显而易见:
def split[A](x: Stream[A]) = {
def goL(v: (Stream[A], Stream[A])): Stream[A] = v match {
case (x #:: xs, _ #:: _ #:: zs) =>
x #:: goL(xs, zs)
case (xs, _) => Stream.empty
}
def goR(v: (Stream[A], Stream[A])): Stream[A] = v match {
case (x #:: xs, _ #:: _ #:: zs) => goR(xs, zs)
case (xs, _) => xs
}
val tup = (x,x)
(goL(tup), () => goR(tup))
}
您可以看到左侧和右侧之间的根本区别:
goL
- 调用被编译器封装在一个闭包中(蹦床模式的“隐藏”版本)goR
的调用手动包装在一个闭包中,否则对goR
的调用不会终止。除了正确的流的闭包之外,这很好用。这可以通过提供流的包装器/视图来缓解,只有在使用它时才会评估基础流(即Stream
对象本身)。
上面的代码可以按如下方式使用:
val (a,b) = split(Stream.continually(1))
println(a.head) // > 1
val (c,d) = split(Stream.fill(1000000)(1))
println(c.size) // > 500000
println(d().size) // > 500000
答案 2 :(得分:1)
你已经以正确的方式做到了。尝试懒惰地进行合并排序是没有意义的。当你打电话给xs.length
时,你已经强迫你的整个流,所以尝试使用一种懒惰的方法来分割它不会有所作为。
你可以做的是让streamMerge
函数变得懒惰。在将已排序的子列表合并在一起时,您只需要知道两个流中每个流的第一个元素,因此在组合流时可以懒惰地确定哪个元素最小。这就是我的想法:
def streamMerge[T](xs: Stream[T], ys: Stream[T])(implicit ord: math.Ordering[T]): Stream[T] = {
if (xs.isEmpty) ys
else if (ys.isEmpty) xs
else {
if (ord.lteq(xs.head, ys.head))
xs.head #:: streamMerge(xs.tail, ys)
else
ys.head #:: streamMerge(xs, ys.tail)
}
}
def streamSort[T](xs: Stream[T])(implicit ord: math.Ordering[T]): Stream[T] = xs match {
case Stream.Empty => xs
case Stream(_) => xs
case _ => {
val m = xs.length / 2
val (l, r) = xs.splitAt(m)
streamMerge(streamSort(l), streamSort(r))
}
}