Scala groupBy 项目列表中的所有元素

Posted

技术标签:

【中文标题】Scala groupBy 项目列表中的所有元素【英文标题】:Scala groupBy all elements in the item's list 【发布时间】:2021-12-03 06:09:50 【问题描述】:

我有一个元组列表,其中第一个元素是字符串,第二个元素是字符串列表。

例如...(忽略语音标记)

val p = List((a, List(x,y,z)), (b, List(x)), (c, List(y,z)))

我的目标是将此列表分组到一个映射中,其中嵌套列表的元素充当键。

val q = Map(x -> List(a,b), y -> List(a,c), z-> List(a,c))

我最初的想法是按 p 的第二个元素进行分组,但这会将整个列表分配给键。

我是 Scala 的初学者,因此不胜感激。我应该期望能够使用更高阶的函数来完成此任务,还是 for 循环在这里有用?

提前致谢:)

【问题讨论】:

感谢大家的回复,我会在星期一查看方法:) 【参考方案1】:

这里有两种变体:

val p = List(("a", List("x","y","z")), ("b", List("x")), ("c", List("y","z")))

// 1. "Transducers"
p.flatMap case (k, v) => v.map  _ -> k     // List((x,a), (y,a), (z,a), (x,b), (y,c), (z,c))
  .groupBy(_._1) // Map(z -> List((z,a), (z,c)), y -> List((y,a), (y,c)), x -> List((x,a), (x,b)))
  .mapValues(_.map(_._2)) // Map(z -> List(a, c), y -> List(a, c), x -> List(a, b))

// 2. For-loop
var res = Map[String, List[String]]()

for ( (k, vs) <- p;  v <- vs) 
  res += v -> k :: res.getOrElse(v, List())


res  // Map(x -> List(b, a), y -> List(c, a), z -> List(c, a))

// Note, values of `res` are inverted, 
// because the efficient "cons" operator (::) was used to add values to the lists
// you can revert the lists afterwards as this:

res.mapValues(_.reverse) // Map(x -> List(a, b), y -> List(a, c), z -> List(a, c))

第二个变体性能更高,因为没有创建中间集合,但它也可以被认为是“不那么惯用的”,因为使用了可变变量res。但是,在私有方法中使用可变方法是完全可以的。


UPD。根据@LuisMiguelMejíaSuárez 的建议:

在(1)中,由于scala 2.13,groupBy后面跟mapValues可以替换为groupMap,所以整个链条变成:

p.flatMap case (k, v) => v.map  _ -> k   
   .groupMap(_._1)(_._2)

另一个没有中间集合的功能变体可以使用foldLeft实现:

p.foldLeft(Map[String, List[String]]()) 
  case (acc, (k, vs)) =>
    vs.foldLeft(acc)  (acc1, v) =>
      acc1 + (v -> (k :: acc1.getOrElse(v, List())))
    

或者使用updatedWith(scala 2.13)更有效:

p.foldLeft(Map[String, List[String]]()) 
  case (acc, (k, vs)) =>
    vs.foldLeft(acc)  (acc1, v) =>
      acc1.updatedWith(v) 
        case Some(list) => Some(k :: list)
        case None       => Some(List(k))
      
    

...或者同样的东西稍微短一点:

p.foldLeft(Map[String, List[String]]()) 
  case (acc, (k, vs)) =>
    vs.foldLeft(acc)  (acc1, v) =>
      acc1.updatedWith(v)(_.map(k :: _).orElse(Some(List(k))))
    

总的来说,我建议根据您的目标使用foldLeft 变体(性能和功能最高)或第一个groupMap 变体(更短,可以说更具可读性,但性能较差)。

【讨论】:

您可以在第一个中使用groupMap 来减少一些中间收集。 - 无论如何,通过使用IteratorfoldLeft 并使用updateWith 来简化逻辑,第二个可以更快、更实用。 类似这样的:(可能有错别字) p.iterator.flatMap case (k, vs) =&gt; vs.Iterator.map(v =&gt; v -&gt; k) .foldLeft(Map.empty[V, List[K])) case (acc, (v, k)) =&gt; acc.updatedWith(key = v) case Some(list) =&gt; Some(k :: list) case None =&gt; Some(Nil) @LuisMiguelMejíaSuárez,谢谢,我已经更新了我的答案,并结合了你的想法。【参考方案2】:

您的输入列表p 距离成为Map 仅一步之遥。从那里您只需要一个通用 Map 逆变器。

import scala.collection.generic.IsIterableOnce
import scala.collection.Factory

// from Map[K,C[V]] to Map[V,C[K]] (Scala 2.13.x)
implicit class MapInverter[K,V,C[_]](m: Map[K,C[V]]) 
  def invert(implicit iio: IsIterableOnce[C[V]] type A = V
                    , fac: Factory[K,C[K]]): Map[V,C[K]] =
    m.foldLeft(Map.empty[V, List[K]]) 
      case (acc, (k, vs)) =>
        iio(vs).iterator.foldLeft(acc) 
          case (a, v) =>
            a + (v -> (k::a.getOrElse(v,Nil)))
        
    .mapcase (k,v) => k -> v.to(fac)

用法:

val p = List(("a", List("x","y","z")), ("b", List("x")), ("c", List("y","z")))
val q = p.toMap.invert
//Map(x -> List(b, a), y -> List(c, a), z -> List(c, a))

【讨论】:

以上是关于Scala groupBy 项目列表中的所有元素的主要内容,如果未能解决你的问题,请参考以下文章

scala如何取出rdd中的每一个元素

Spark(scala):groupby和聚合值列表到一个基于索引的列表[重复]

2021年大数据常用语言Scala(二十六):函数式编程 分组 groupBy

从列表中搜索数据框以及在 Scala 的新列中找到的所有元素

Scala语言之高阶函数

Scala中的列表可以添加元素吗?