【问题标题】:Cleaner tuple groupBy更干净的元组 groupBy
【发布时间】:2020-04-19 22:40:34
【问题描述】:

我有一个键值对序列(String、Int),我想通过键将它们组合成一个值序列(即Seq[(String, Int)]) => Map[String, Iterable[Int]]))。

显然,toMap 在这里没有用,groupBy 将值维护为元组。我想出的最好办法是:

val seq: Seq[( String, Int )]
// ...
seq.groupBy( _._1 ).mapValues( _.map( _._2 ) )

有没有更清洁的方法?

【问题讨论】:

  • +1 经常使用这种模式,我希望有内置的东西。

标签: scala scala-collections


【解决方案1】:

这是一个将toMultiMap 方法添加到可遍历对象的皮条客。能解决你的问题吗?

import collection._
import mutable.Builder
import generic.CanBuildFrom

class TraversableOnceExt[CC, A](coll: CC, asTraversable: CC => TraversableOnce[A]) {

  def toMultiMap[T, U, That](implicit ev: A <:< (T, U), cbf: CanBuildFrom[CC, U, That]): immutable.Map[T, That] =
    toMultiMapBy(ev)

  def toMultiMapBy[T, U, That](f: A => (T, U))(implicit cbf: CanBuildFrom[CC, U, That]): immutable.Map[T, That] = {
    val mutMap = mutable.Map.empty[T, mutable.Builder[U, That]]
    for (x <- asTraversable(coll)) {
      val (key, value) = f(x)
      val builder = mutMap.getOrElseUpdate(key, cbf(coll))
      builder += value
    }
    val mapBuilder = immutable.Map.newBuilder[T, That]
    for ((k, v) <- mutMap)
      mapBuilder += ((k, v.result))
    mapBuilder.result
  }
}

implicit def commomExtendTraversable[A, C[A] <: TraversableOnce[A]](coll: C[A]): TraversableOnceExt[C[A], A] =
  new TraversableOnceExt[C[A], A](coll, identity)

可以这样使用:

val map = List(1 -> 'a', 1 -> 'à', 2 -> 'b').toMultiMap
println(map)  // Map(1 -> List(a, à), 2 -> List(b))

val byFirstLetter = Set("abc", "aeiou", "cdef").toMultiMapBy(elem => (elem.head, elem))
println(byFirstLetter) // Map(c -> Set(cdef), a -> Set(abc, aeiou))

如果您添加以下隐式定义,它也适用于类似集合的对象,例如 Strings 和 Arrays:

implicit def commomExtendStringTraversable(string: String): TraversableOnceExt[String, Char] =
  new TraversableOnceExt[String, Char](string, implicitly)

implicit def commomExtendArrayTraversable[A](array: Array[A]): TraversableOnceExt[Array[A], A] =
  new TraversableOnceExt[Array[A], A](array, implicitly)

然后:

val withArrays = Array(1 -> 'a', 1 -> 'à', 2 -> 'b').toMultiMap
println(withArrays) // Map(1 -> [C@377653ae, 2 -> [C@396fe0f4)

val byLowercaseCode = "Mama".toMultiMapBy(c => (c.toLower.toInt, c))
println(byLowercaseCode) // Map(97 -> aa, 109 -> Mm)

【讨论】:

  • 远远超出我的预期,但仍然有用。谢谢!
  • 这很好。有没有一种简单的方法来覆盖值集合的类型(比如我有一个List(1 -&gt; 'a', 1 -&gt; 'à', 2 -&gt; 'b'),但我希望toMultiMap 的结果是Map[Int, Set[String]]?也许breakOut 有一些技巧?
【解决方案2】:

标准库中没有执行此操作的方法或数据结构,您的解决方案看起来尽可能简洁。如果您在多个地方使用它,您可能希望将其分解为实用方法

def groupTuples[A, B](seq: Seq[(A, B)]) = 
  seq groupBy (_._1) mapValues (_ map (_._2))

然后你显然只是用groupTuples(seq) 打电话。就 CPU 时钟周期而言,这可能不是最有效的,但我认为它也不是特别低效。

我对 Jean-Philippe 的解决方案在 9 个元组的列表上做了一个粗略的基准测试,这稍微快了一点。两者的速度大约是将序列折叠成地图的两倍(有效地重新实现 groupBy 以提供所需的输出)。

【讨论】:

  • mapValues 实际上只是包装了构建的地图,因此在地图中查找内容时效率可能会降低。此外,我已经编辑了我的答案以避免toMap;你介意(出于好奇)再次运行相同的基准测试吗?根据我的基准,使用 9 个元组,构建地图和两次查找的速度大约快了三分之一。
  • @Jean-Philippe 我在上面运行 100k 时得到 97ms,而更新后的运行得到 106ms。当然,我们真的应该尝试不同的列表长度和组成,但我只是想得到一个大概的想法。实际上,它们的速度相同。
  • @Jean-Philippe 对 mapValues 包装现有地图很感兴趣 - 我不知道。使用seq groupBy (_._1) map (x =&gt; (x._1, x._2 map (_._2))) 创建一个全新的地图需要 165 毫秒,因此在内存中创建一个新地图更快。
  • 效率实际上并不是一个大问题,但很高兴知道效果无论如何。很遗憾我不能接受两个答案 - 我将不得不接受 Jean-Philippe 的答案,因为它是如此全面:-)
【解决方案3】:

我不知道你是否认为它更干净:

seq.groupBy(_._1).map { case (k,v) => (k,v.map(_._2))}

【讨论】:

  • 我没有,真的。正在寻找更简洁和/或语义清晰的解决方案。
  • 当然。我想您将不得不为此创建一个函数并在需要时调用它。这样你就可以按照你想要的方式制作语法。
【解决方案4】:

从Scala 2.13 开始,大多数集合都提供groupMap 方法(顾名思义)等效于(更有效)groupBy 后跟mapValues:

List(1 -> 'a', 1 -> 'b', 2 -> 'c').groupMap(_._1)(_._2)
// Map[Int,List[Char]] = Map(2 -> List(c), 1 -> List(a, b))

这个:

  • groups 元素基于元组的第一部分 (Map(2 -&gt; List((2,c)), 1 -&gt; List((1,a), (1,b))))

  • maps 分组值 (List((1,a), (1,b))) 通过获取它们的第二个元组部分 (List(a, b))。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-09-02
    • 2012-10-24
    • 1970-01-01
    • 1970-01-01
    • 2018-04-07
    • 2019-12-08
    • 2015-02-13
    相关资源
    最近更新 更多