【问题标题】:Scala: Functional aggregation of Seq[T] elements => Seq[Seq[T]] (preserving order)Scala:Seq[T] 元素的功能聚合 => Seq[Seq[T]](保留顺序)
【发布时间】:2011-12-26 12:50:14
【问题描述】:

我想聚合序列中的兼容元素,即将Seq[T] 转换为Seq[Seq[T]],其中每个子序列中的元素相互兼容,同时保留原始 seq 顺序,例如来自

case class X(i: Int, n: Int) {
  def canJoin(that: X): Boolean = this.n == that.n
  override val toString = i + "." + n
}
val xs = Seq(X(1, 1), X(2, 3), X(3, 3), X(4, 3), X(5, 1), X(6, 2), X(7, 2), X(8, 1))
/* xs = List(1.1, 2.3, 3.3, 4.3, 5.1, 6.2, 7.2, 8.1) */

想得到

val js = join(xs)
/* js = List(List(1.1), List(2.3, 3.3, 4.3), List(5.1), List(6.2, 7.2), List(8.1)) */

我试图以一种实用的方式来做这件事,但我半途而废:

使用 while 循环

def split(seq: Seq[X]): (Seq[X], Seq[X]) = seq.span(_ canJoin seq.head)
def join(seq: Seq[X]): Seq[Seq[X]] = {
  var pp = Seq[Seq[X]]()
  var s = seq
  while (!s.isEmpty) {
    val (p, r) = split(s)
    pp :+= p
    s = r
  }
  pp
}

split 我很满意,但join 似乎有点太长了。

在我看来,这是一项标准任务。这引出了我的问题:

  1. 收藏库中是否有函数可以实现 可以减少代码大小吗?
  2. 或者是否有不同的方法来解决这个任务?特别是另一个 比Rewriting a sequence by partitioning and collapsing更接近?

用尾递归替换while循环

def join(xs: Seq[X]): Seq[Seq[X]] = {
  @annotation.tailrec
  def jointr(pp: Seq[Seq[X]], rem: Seq[X]): Seq[Seq[X]] = {
    val (p, r) = split(rem)
    val pp2 = pp :+ p
    if (r.isEmpty) pp2 else jointr(pp2, r)
  }
  jointr(Seq(), xs)
}

【问题讨论】:

  • 所以我们同时发布了几乎相同的答案/编辑:D
  • 我的答案不是尾递归,因为最后一个操作是 Seq-concatenation。
  • 我会将jointr 作为辅助函数放入join 的定义中。
  • @Peter Schmitz:按照建议让jointr成为join的助手def。
  • 这有点过头了,但是用元素对做一些事情是很常见的,我在我的例子中使用它来拉皮条集合库:stackoverflow.com/questions/5410846。有了那个皮条客,它就是xs.groupedWhile(_ canJoin _)(也就是说,它解决了同样的问题)。这并不简单,但它具有最大的灵活性(返回正确的类型、适用于数组等)。

标签: scala collections functional-programming


【解决方案1】:
def join(seq: Seq[X]): Seq[Seq[X]] = {
  if (seq.isEmpty) return Seq()
  val (p,r) = split(seq)
  Seq(p) ++ join(r)
}

【讨论】:

  • 非常好(简短易懂):-)
【解决方案2】:

这里是foldLeft版本:

def join(seq: Seq[X]) = xs.reverse.foldLeft(Nil: List[List[X]]) {
    case ((top :: group) :: rest, x) if x canJoin top => 
        (x :: top :: group) :: rest
    case (list, x) => (x :: Nil) :: list
} 

foldRight 版本(在这种情况下你不需要reverse 列表):

def join(seq: Seq[X]) = xs.foldRight(Nil: List[List[X]]) {
    case (x, (top :: group) :: rest) if x canJoin top => 
        (x :: top :: group) :: rest
    case (x, list) => (x :: Nil) :: list
} 

【讨论】:

  • 我试过了,效果很好。我只是还不知道它是如何工作的,因为我对列表不太熟悉,并且可以直观地处理。但在一个安静的时刻,我会再仔细看看,代码看起来确实不错。
【解决方案3】:

基准测试

因为我有太多时间 ;-),所以我问自己,因为它是通过不同方法的运行时来感受一个沉重的结构是否潜伏在轻量级语法后面。

所以我创建了一个微基准来测量三个序列的运行时间

(1, 3, 3, 3, 1, 2, 2, 1)
(1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 8, 8, 7, 6, 5, 4, 3, 3, 3, 2, 1, 2, 3)
(2, 2, 3, 4, 5, 6, 7, 8, 8, 8, 8, 8, 8, 8, 8, 7, 6, 5, 4, 4, 4, 4, 3, 3, 3, 2, 1)

得到如下结果:

总结

编辑:新结果(开始):

在我的实际项目中纳入结果时,我遇到了关于基准的不一致。所以我用更多的热身圈(现在是 1000 圈)再次重复了基准测试,这样 JIT 编译器就可以充分利用代码。因此,对结果进行了改组,并为我带来了一个新的最爱:X7 (pimp my lib) = 无悔的快乐List 版本 X8 (reverse.foldLeft) 现在也非常快。

Nr (Approach)                      Running time (ns)  Contributor
X2 (poor.reference.impl)            in    15.202 ns
X1 (original while loop)            in     8.166 ns
X3 (tail recursion)                 in     7.473 ns
X4 (recursion with ++)              in     6.671 ns   Peter Schmitz
X5 (simplified recursion with ++)   in     6.161 ns   Peter Schmitz
X6 (foldRight)                      in     4.083 ns   tenshi
X7 (pimp my lib)                    in     1.677 ns   Rex Kerr
X8 (reverse.foldLeft)               in     1.349 ns   tenshi

编辑:新结果(结束)

旧结果:

Nr (Approach)                      Running time (ns)  Contributor
X2 (poor.reference.impl)            in 2.972.015 ns
X7 (pimp my lib)                    in 1.185.599 ns   Rex Kerr
X3 (tail recursion)                 in 1.027.008 ns
X8 (reverse.foldLeft)               in   643.840 ns   tenshi
X6 (foldRight)                      in   608.112 ns   ""
X1 (original while loop)            in   564.726 ns
X4 (recursion with ++)              in   468.478 ns   Peter Schmitz
X5 (simplified recursion with ++)   in   447.699 ns   ""

详情

X2 (poor.reference.impl)

// in    15.202 ns
import collection.mutable.ArrayBuffer
def join2(seq: Seq[X]): Seq[Seq[X]] = {
  var pp = Seq[ArrayBuffer[X]](ArrayBuffer(seq(0)))
  for (i <- 1 until seq.size) {
    if (seq(i) canJoin seq(i - 1)) {
      pp.last += seq(i)
    } else {
      pp :+= ArrayBuffer(seq(i))
    }
  }
  pp
}

X1(while 循环)

// in     8.166 ns
def join(xs: Seq[X]): Seq[Seq[X]] = {
  var xss = Seq.empty[Seq[X]]
  var s = xs
  while (!s.isEmpty) {
    val (p, r) = split(s)
    xss :+= p
    s = r
  }
  xss
}

这是问题开头的原始命令式方法。

X3​​(尾递归)

// in     7.473 ns
def join(xs: Seq[X]): Seq[Seq[X]] = {
  @annotation.tailrec
  def jointr(xss: Seq[Seq[X]], rxs: Seq[X]): Seq[Seq[X]] = {
    val (g, r) = split(rxs)
    val xsn = xss :+ g
    if (r.isEmpty) xsn else jointr(xsn, r)
  }
  jointr(Seq(), xs)
}

X4(用++递归)

// in     6.671 ns
def join(seq: Seq[X]): Seq[Seq[X]] = {
  if (seq.isEmpty) return Seq()
  val (p, r) = split(seq)
  Seq(p) ++ join(r)
}

X5(使用 ++ 的简化递归)

// in     6.161 ns
def join(xs: Seq[X]): Seq[Seq[X]] = if (xs.isEmpty) Seq() else {
  val (p, r) = split(xs)
  Seq(p) ++ join(r)
}

简化几乎相同,但仍然快一点。

X6 (foldRight)

// in     4.083 ns
def join(xs: Seq[X]) = xs.foldRight(Nil: List[List[X]]) {
  case (x, (top :: group) :: rest) if x canJoin top => (x :: top :: group) :: rest
  case (x, list)                                    => (x :: Nil) :: list
}

试图避免reverse,但foldRight 似乎比reverse.foldLeft 更糟糕。

X7(皮条客我的库)

// in     1.677 ns
import collection.generic.CanBuildFrom
class GroupingCollection[A, C, D[C]](ca: C)(
    implicit c2i: C => Iterable[A],
    cbf: CanBuildFrom[C, C, D[C]],
    cbfi: CanBuildFrom[C, A, C]) {
  def groupedWhile(p: (A, A) => Boolean): D[C] = {
    val it = c2i(ca).iterator
    val cca = cbf()
    if (!it.hasNext) cca.result
    else {
      val as = cbfi()
      var olda = it.next
      as += olda
      while (it.hasNext) {
        val a = it.next
        if (p(olda, a)) as += a
        else { cca += as.result; as.clear; as += a }
        olda = a
      }
      cca += as.result
    }
    cca.result
  }
}
implicit def collections_have_grouping[A, C[A]](ca: C[A])(
  implicit c2i: C[A] => Iterable[A],
  cbf: CanBuildFrom[C[A], C[A], C[C[A]]],
  cbfi: CanBuildFrom[C[A], A, C[A]]) = {
  new GroupingCollection[A, C[A], C](ca)(c2i, cbf, cbfi)
}
// xs.groupedWhile(_ canJoin _)

X8 (reverse.foldLeft)

// in     1.349 ns
def join(xs: Seq[X]) = xs.reverse.foldLeft(Nil: List[List[X]]) {
  case ((top :: group) :: rest, x) if x canJoin top => (x :: top :: group) :: rest
  case (list, x)                                    => (x :: Nil) :: list
}

结论

不同的方法(X1、X3、X4、X5、X6)都在同一个联赛中发挥作用。

因为 X7 (pimp my lib) 允许非常简洁的用法 xs.groupedWhile(_ canJoin _) 并导致非常必要的代码可以隐藏在自己的 util lib 中,所以我决定在我的实际应用中使用它项目。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-05-26
    • 1970-01-01
    • 2011-04-04
    相关资源
    最近更新 更多