Scala:如何合并地图集合

Posted

技术标签:

【中文标题】Scala:如何合并地图集合【英文标题】:Scala: how to merge a collection of Maps 【发布时间】:2010-11-18 18:19:47 【问题描述】:

我有一个 Map[String, Double] 列表,我想将它们的内容合并到一个 Map[String, Double] 中。我应该如何以惯用的方式做到这一点?我想我应该可以通过弃牌做到这一点。比如:

val newMap = Map[String, Double]() /: listOfMaps  (accumulator, m) => ... 

此外,我想以通用方式处理键冲突。也就是说,如果我向已经存在的映射添加一个键,我应该能够指定一个返回 Double 的函数(在这种情况下)并获取该键的现有值,加上我试图添加的值.如果映射中尚不存在该键,则只需添加它且其值不变。

在我的具体情况下,我想构建一个 Map[String, Double] ,这样如果地图已经包含一个键,那么 Double 将被添加到现有的地图值中。

我在我的特定代码中使用可变映射,但如果可能的话,我对更通用的解决方案感兴趣。

【问题讨论】:

【参考方案1】:

Scala 2.13 开始,另一个处理重复键并且仅基于标准库的解决方案是将Maps 合并为序列(flatten ) 在应用新的groupMapReduce 运算符之前,它(顾名思义)相当于groupBy,后跟一个映射和分组值的归约步骤:

List(Map("hello" -> 1.1, "world" -> 2.2), Map("goodbye" -> 3.3, "hello" -> 4.4))
  .flatten
  .groupMapReduce(_._1)(_._2)(_ + _)
// Map("world" -> 2.2, "goodbye" -> 3.3, "hello" -> 5.5)

这个:

flattens(连接)将映射作为元组序列 (List(("hello", 1.1), ("world", 2.2), ("goodbye", 3.3), ("hello", 4.4))),保留所有键/值(甚至是重复键)

groups 元素基于它们的第一个元组部分 (_._1)(groupMapReduce 的组部分)

maps 将值分组到它们的第二个元组部分 (_._2)(组的映射部分MapReduce)

reduces 映射分组值 (_+_) 通过取它们的总和(但它可以是任何 reduce: (T, T) => T 函数)(减少 groupMap 的一部分Reduce


groupMapReduce 步骤可以看作是 one-pass version 等价于:

list.groupBy(_._1).mapValues(_.map(_._2).reduce(_ + _))

【讨论】:

【参考方案2】:

我很惊讶还没有人提出这个解决方案:

myListOfMaps.flatten.toMap

完全满足您的需要:

    将列表合并到一个地图 清除所有重复键

例子:

scala> List(Map('a -> 1), Map('b -> 2), Map('c -> 3), Map('a -> 4, 'b -> 5)).flatten.toMap
res7: scala.collection.immutable.Map[Symbol,Int] = Map('a -> 4, 'b -> 5, 'c -> 3)

flatten 将映射列表转换为元组的平面列表,toMap 将元组列表转换为删除所有重复键的映射

【讨论】:

这正是我所需要的,但不会像 OP 要求的那样对重复键的值求和。 或者你可以使用flatMap @wbmrcb 在这种情况下你会如何使用 flatMap?使用 flatMap,扁平化发生在映射之后,但这里是相反的。那么它是如何工作的呢? @electric-Coffee 如果每个 Map 都包含多个键值对,则这只需要最后一个 Map【参考方案3】:

我写了一篇关于这个的博客文章,看看:

http://www.nimrodstech.com/scala-map-merge/

基本上使用 scalaz semi 组你可以很容易地做到这一点

看起来像:

  import scalaz.Scalaz._
  listOfMaps reduce(_ |+| _)

【讨论】:

其实可以用listOfMaps.suml;它应该做同样的事情。据我了解,这意味着 sumLeft,它基本上运行 reduceLeft(_ |+| _)【参考方案4】:

oneliner helper-func,其用法几乎与使用 scalaz 一样干净:

def mergeMaps[K,V](m1: Map[K,V], m2: Map[K,V])(f: (V,V) => V): Map[K,V] =
    (m1 -- m2.keySet) ++ (m2 -- m1.keySet) ++ (for (k <- m1.keySet & m2.keySet) yield  k -> f(m1(k), m2(k)) )

val ms = List(Map("hello" -> 1.1, "world" -> 2.2), Map("goodbye" -> 3.3, "hello" -> 4.4))
ms.reduceLeft(mergeMaps(_,_)(_ + _))
// returns Map(goodbye -> 3.3, hello -> 5.5, world -> 2.2)

为了最终的可读性,将其包装在隐式自定义类型中:

class MyMap[K,V](m1: Map[K,V]) 
    def merge(m2: Map[K,V])(f: (V,V) => V) =
    (m1 -- m2.keySet) ++ (m2 -- m1.keySet) ++ (for (k <- m1.keySet & m2.keySet) yield  k -> f(m1(k), m2(k)) )

implicit def toMyMap[K,V](m: Map[K,V]) = new MyMap(m)

val ms = List(Map("hello" -> 1.1, "world" -> 2.2), Map("goodbye" -> 3.3, "hello" -> 4.4))
ms reduceLeft  _.merge(_)(_ + _)  

【讨论】:

【参考方案5】:

我很快就阅读了这个问题,所以我不确定我是否遗漏了什么(比如它必须适用于 2.7.x 或没有 scalaz):

import scalaz._
import Scalaz._
val ms = List(Map("hello" -> 1.1, "world" -> 2.2), Map("goodbye" -> 3.3, "hello" -> 4.4))
ms.reduceLeft(_ |+| _)
// returns Map(goodbye -> 3.3, hello -> 5.5, world -> 2.2)

您可以更改 Double 的 monoid 定义并获得另一种累积值的方法,这里获取最大值:

implicit val dbsg: Semigroup[Double] = semigroup((a,b) => math.max(a,b))
ms.reduceLeft(_ |+| _)
// returns Map(goodbye -> 3.3, hello -> 4.4, world -> 2.2)

【讨论】:

+1,虽然我会写 ms.suml,它更简洁,并且具有不会在空列表上引发运行时异常的额外优势。 @TravisBrown,是的,scalaz 中有这么多方便的功能;虽然suml 可能只是scalaz 7?我只在 6.x 中看到 sumr【参考方案6】:

这个怎么样:

def mergeMap[A, B](ms: List[Map[A, B]])(f: (B, B) => B): Map[A, B] =
  (Map[A, B]() /: (for (m <- ms; kv <- m) yield kv))  (a, kv) =>
    a + (if (a.contains(kv._1)) kv._1 -> f(a(kv._1), kv._2) else kv)
  

val ms = List(Map("hello" -> 1.1, "world" -> 2.2), Map("goodbye" -> 3.3, "hello" -> 4.4))
val mm = mergeMap(ms)((v1, v2) => v1 + v2)

println(mm) // prints Map(hello -> 5.5, world -> 2.2, goodbye -> 3.3)

它适用于 2.7.5 和 2.8.0。

【讨论】:

这正是我最初尝试的方式。我不认为将理解放在那里——我仍然习惯于这样使用它们,但这是有道理的。在这种情况下,我可以看到它很像 Python 的列表推导式,我对此感到更舒服。也喜欢在对 a.+() 的调用中使用带有结果的 if 表达式。 非常感谢!,我做了一点改动,我没有收到List[Map[A,B]],而是将其更改为Seq[Map[A,B]],因此它更通用,您可以避免调用ms.toList,例如msArrayBuffer,就我而言。【参考方案7】:

好吧,你可以这样做:

mapList reduce (_ ++ _)

碰撞的特殊要求除外。

既然你确实有这个特殊要求,也许最好的办法是做这样的事情(2.8):

def combine(m1: Map, m2: Map): Map = 
  val k1 = Set(m1.keysIterator.toList: _*)
  val k2 = Set(m2.keysIterator.toList: _*)
  val intersection = k1 & k2

  val r1 = for(key <- intersection) yield (key -> (m1(key) + m2(key)))
  val r2 = m1.filterKeys(!intersection.contains(_)) ++ m2.filterKeys(!intersection.contains(_)) 
  r2 ++ r1

然后您可以通过 Pimp My Library 模式将此方法添加到地图类中,并在原始示例中使用它而不是“++”:

class CombiningMap(m1: Map[Symbol, Double]) 
  def combine(m2: Map[Symbol, Double]) = 
    val k1 = Set(m1.keysIterator.toList: _*)
    val k2 = Set(m2.keysIterator.toList: _*)
    val intersection = k1 & k2
    val r1 = for(key <- intersection) yield (key -> (m1(key) + m2(key)))
    val r2 = m1.filterKeys(!intersection.contains(_)) ++ m2.filterKeys(!intersection.contains(_))
    r2 ++ r1
  


// Then use this:
implicit def toCombining(m: Map[Symbol, Double]) = new CombiningMap(m)

// And finish with:
mapList reduce (_ combine _)

虽然这是用 2.8 编写的,所以对于 2.7,keysIterator 变为 keysfilterKeys 可能需要以 filtermap 的形式编写,&amp; 变为 **,等等开,应该不会差太多。

【讨论】:

使用现代 Scala:val k1 = m1.keysIterator.toSet【参考方案8】:

有趣的是,在这附近闲逛了一下,我得到了以下内容(在 2.7.5 上):

一般地图:

   def mergeMaps[A,B](collisionFunc: (B,B) => B)(listOfMaps: Seq[scala.collection.Map[A,B]]): Map[A, B] = 
    listOfMaps.foldLeft(Map[A, B]())  (m, s) =>
      Map(
        s.projection.map  pair =>
        if (m contains pair._1)
          (pair._1, collisionFunc(m(pair._1), pair._2))
        else
          pair
      .force.toList:_*)
    
  

但是,伙计,这对于投影和强制以及 toList 和诸如此类的东西来说是可怕的。单独的问题:在折叠内处理这个问题的更好方法是什么?

对于可变地图,这是我在代码中处理的内容,并且使用不太通用的解决方案,我得到了这个:

def mergeMaps[A,B](collisionFunc: (B,B) => B)(listOfMaps: List[mutable.Map[A,B]]): mutable.Map[A, B] = 
    listOfMaps.foldLeft(mutable.Map[A,B]()) 
      (m, s) =>
      for (k <- s.keys) 
        if (m contains k)
          m(k) = collisionFunc(m(k), s(k))
        else
          m(k) = s(k)
      
      m
    
  

这似乎更简洁一些,但仅适用于可变映射,因为它是编写的。有趣的是,我首先尝试使用/:而不是 foldLeft 进行上述操作(在我提出问题之前),但我遇到了类型错误。我认为 /: 和 foldLeft 基本上是等价的,但是编译器一直抱怨我需要 (m, s) 的显式类型。这是怎么回事?

【讨论】:

这里不需要使用force,因为toList是严格的。 至于foldLeft vs /:,你确实意识到对象和第一个参数在它们之间交换了吗?表达式x foldLeft y 等价于y /: x。除此之外,还有一堆语法问题。基本上,你必须(y /: x) (folding expression),而foldLeft可以用作x.foldLeft(y)(folding expression) 是的,我知道以 : 结尾的方法用参数交换对象。这就是我在问题中编写示例的方式。不过,我确实忘记将 y /: x 放在括号中,我敢打赌这是个问题。谢谢!

以上是关于Scala:如何合并地图集合的主要内容,如果未能解决你的问题,请参考以下文章

Scala:将地图列表与每个键的最大值合并的惯用方法?

在scala中合并两个嵌套地图

斯卡拉:如何合并的地图集合

Scala 高阶:集合内容汇总(下篇)

Firebase 查询集合和合并子集合数据

如何在 Scala 中以元组为键合并 Maps