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
开始,另一个处理重复键并且仅基于标准库的解决方案是将Map
s 合并为序列(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)
这个:
flatten
s(连接)将映射作为元组序列 (List(("hello", 1.1), ("world", 2.2), ("goodbye", 3.3), ("hello", 4.4))
),保留所有键/值(甚至是重复键)
group
s 元素基于它们的第一个元组部分 (_._1
)(groupMapReduce 的组部分)
map
s 将值分组到它们的第二个元组部分 (_._2
)(组的映射部分MapReduce)
reduce
s 映射分组值 (_+_
) 通过取它们的总和(但它可以是任何 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
,例如ms
是 ArrayBuffer
,就我而言。【参考方案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
变为 keys
,filterKeys
可能需要以 filter
和 map
的形式编写,&
变为 **
,等等开,应该不会差太多。
【讨论】:
使用现代 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:如何合并地图集合的主要内容,如果未能解决你的问题,请参考以下文章