以功能方式拆分流

时间:2013-09-25 05:49:40

标签: algorithm scala sorting

合并排序例程中有以下片段

val m = xs.length / 2
val (l, r) = xs.splitAt(m)
streamMerge(streamSort(l), streamSort(r))

是否有更功能(和懒惰)的方法将流拆分为两个?我已经尝试从这里http://en.literateprograms.org/Merge_sort_(Haskell)移植拆分例程,但它会导致堆栈溢出崩溃。

3 个答案:

答案 0 :(得分:3)

我看到两种可能性:不要使用长度或不使用流。

不要使用长度

长度是流的严格函数,所以你不能这样做。但是存在多种非严格的可能性:

  1. 使用概率方法
  2. 使用每个第n个元素
  3. 概率方法

    从流中获取前三个元素。当少于三个时,首先分裂是​​没有任何意义的。然后使用Stream.partition(_ > biggestElement)分割大于这三个元素的第一个元素。

    这通常可以很好地工作,但在特殊数据星座上会有问题(例如输入已经排序)。

    使用每个第n个元素

    均匀分割流,但不在中间。使用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))
  }
}