合并两个映射并求同一键值的最佳方法?

val map1 = Map(1 -> 9 , 2 -> 20)
val map2 = Map(1 -> 100, 3 -> 300)

我想把它们合并,然后把相同键的值加起来,结果是:

Map(2->20, 1->109, 3->300)

现在我有两个解决办法:

val list = map1.toList ++ map2.toList
val merged = list.groupBy ( _._1) .map { case (k,v) => k -> v.map(_._2).sum }

还有

val merged = (map1 /: map2) { case (map, (k,v)) =>
map + ( k -> (v + map.getOrElse(k, 0)) )
}

但我想知道是否有更好的解决办法。

96699 次浏览

Scalaz 有一个 半群的概念,它可以捕捉到你想要做的事情,并且可以说是最短/最干净的解决方案:

scala> import scalaz._
import scalaz._


scala> import Scalaz._
import Scalaz._


scala> val map1 = Map(1 -> 9 , 2 -> 20)
map1: scala.collection.immutable.Map[Int,Int] = Map(1 -> 9, 2 -> 20)


scala> val map2 = Map(1 -> 100, 3 -> 300)
map2: scala.collection.immutable.Map[Int,Int] = Map(1 -> 100, 3 -> 300)


scala> map1 |+| map2
res2: scala.collection.immutable.Map[Int,Int] = Map(1 -> 109, 3 -> 300, 2 -> 20)

具体来说,Map[K, V]的二进制运算符组合了映射的键,将 V的半群运算符折叠到任何重复值上。Int的标准半群使用加法运算符,因此可以得到每个重复键的值之和。

编辑 : 根据用户482745的请求,提供更多细节。

从数学上讲,半群只是一组值,再加上一个操作符,该操作符从该集合获取两个值,并从该集合生成另一个值。因此,加法下的整数是一个半群,例如,+运算符将两个整数组合成另一个整数。

你也可以在“具有给定键类型和值类型的所有映射”的集合上定义一个半群,只要你能想出一些操作,将两个映射结合起来生成一个新的,以某种方式将两个输入结合起来。

如果两个映射中都没有出现键,那么这就很简单了。如果两个映射中存在相同的键,那么我们需要将键映射到的两个值组合起来。嗯,我们不是刚刚描述了一个结合了两个相同类型的实体的运算符吗?这就是为什么在 Scalaz,当且仅当 V的半群存在时,Map[K, V]的半群才存在—— V的半群被用来组合分配给同一个键的两个映射的值。

因为 Int是这里的值类型,所以 1键上的“冲突”是通过两个映射值的整数相加来解决的(Int 的半群运算符就是这样做的) ,因此是 100 + 9。如果这些值是 String,那么碰撞将导致两个映射值的字符串串联(同样,因为这是 String 的半群运算符所做的)。

(有趣的是,因为字符串连接不是可交换的,也就是 "a" + "b" != "b" + "a",所以结果的半群操作也不是。因此,map1 |+| map2在 String 情况下与 map2 |+| map1不同,但在 Int 情况下不同。)

快速解决方案:

(map1.keySet ++ map2.keySet).map {i=> (i,map1.getOrElse(i,0) + map2.getOrElse(i,0))}.toMap

据我所知,只使用标准库的最短答案是

map1 ++ map2.map{ case (k,v) => k -> (v + map1.getOrElse(k,0)) }
map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )

好的,现在在 scala 库(至少在2.10中)中有一些你想要的东西—— 合并了函数。但是它只在 HashMap 中显示,而不在 Map 中。有点让人困惑。而且签名也很麻烦——无法想象为什么我需要两次钥匙,什么时候我需要另一把钥匙。尽管如此,它仍然有效,而且比以前的“本地”解决方案干净得多。

val map1 = collection.immutable.HashMap(1 -> 11 , 2 -> 12)
val map2 = collection.immutable.HashMap(1 -> 11 , 2 -> 12)
map1.merged(map2)({ case ((k,v1),(_,v2)) => (k,v1+v2) })

也在 Scaladoc 提到过

merged方法的平均性能比执行 遍历和重构一个新的不可变哈希映射 或者说 ++

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

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

基本上使用 scalaz 半组,你可以很容易地实现这一点

会是这样的:

  import scalaz.Scalaz._
map1 |+| map2

我有一个小函数来完成这项工作,它在我的小库中,用于一些常用的功能,这些功能不在标准库中。 它应该适用于所有类型的映射,可变的和不可变的,而不仅仅是 HashMaps

这是用法

scala> import com.daodecode.scalax.collection.extensions._
scala> val merged = Map("1" -> 1, "2" -> 2).mergedWith(Map("1" -> 1, "2" -> 2))(_ + _)
merged: scala.collection.immutable.Map[String,Int] = Map(1 -> 2, 2 -> 4)

Https://github.com/jozic/scalax-collection/blob/master/readme.md#mergedwith

这是尸体

def mergedWith(another: Map[K, V])(f: (V, V) => V): Repr =
if (another.isEmpty) mapLike.asInstanceOf[Repr]
else {
val mapBuilder = new mutable.MapBuilder[K, V, Repr](mapLike.asInstanceOf[Repr])
another.foreach { case (k, v) =>
mapLike.get(k) match {
case Some(ev) => mapBuilder += k -> f(ev, v)
case _ => mapBuilder += k -> v
}
}
mapBuilder.result()
}

Https://github.com/jozic/scalax-collection/blob/master/src%2fmain%2fscala%2fcom%2fdaodecode%2fscalax%2fcollection%2fextensions%2fpackage.scala#l190

这是我想出来的。

def mergeMap(m1: Map[Char, Int],  m2: Map[Char, Int]): Map[Char, Int] = {
var map : Map[Char, Int] = Map[Char, Int]() ++ m1
for(p <- m2) {
map = map + (p._1 -> (p._2 + map.getOrElse(p._1,0)))
}
map
}

这可以通过普通的 Scala 实现为 Monoid。下面是一个示例实现。使用这种方法,我们不仅可以合并2个,还可以合并一个映射列表。

// Monoid trait


trait Monoid[M] {
def zero: M
def op(a: M, b: M): M
}

合并两个映射的 Monoid trait 的基于 Map 的实现。

val mapMonoid = new Monoid[Map[Int, Int]] {
override def zero: Map[Int, Int] = Map()


override def op(a: Map[Int, Int], b: Map[Int, Int]): Map[Int, Int] =
(a.keySet ++ b.keySet) map { k =>
(k, a.getOrElse(k, 0) + b.getOrElse(k, 0))
} toMap
}

现在,如果您有一个需要合并的映射列表(在本例中,只有2个) ,那么可以像下面这样做。

val map1 = Map(1 -> 9 , 2 -> 20)
val map2 = Map(1 -> 100, 3 -> 300)


val maps = List(map1, map2) // The list can have more maps.


val merged = maps.foldLeft(mapMonoid.zero)(mapMonoid.op)

你也可以用 做到这一点。

import cats.implicits._


val map1 = Map(1 -> 9 , 2 -> 20)
val map2 = Map(1 -> 100, 3 -> 300)


map1 combine map2 // Map(2 -> 20, 1 -> 109, 3 -> 300)

Andrzej Doyle 的答案包含了对半群的很好的解释,它允许您使用 |+|运算符来连接两个映射,并对匹配键的值进行求和。

有许多方法可以定义某个类型类的实例,与 OP 不同的是,您可能不希望专门对键进行求和。或者,您可能希望在联合上进行操作,而不是在十字路口进行操作。Scalaz 还为此向 Map添加了额外的函数:

Https://oss.sonatype.org/service/local/repositories/snapshots/archive/org/scalaz/scalaz_2.11/7.3.0-snapshot/scalaz_2.11-7.3.0-snapshot-javadoc.jar/!/index.html#scalaz.std 地图函数

你可以的

import scalaz.Scalaz._


map1 |+| map2 // As per other answers
map1.intersectWith(map2)(_ + _) // Do things other than sum the values

最快最简单的方法:

val m1 = Map(1 -> 1.0, 3 -> 3.0, 5 -> 5.2)
val m2 = Map(0 -> 10.0, 3 -> 3.0)
val merged = (m2 foldLeft m1) (
(acc, v) => acc + (v._1 -> (v._2 + acc.getOrElse(v._1, 0.0)))
)

通过这种方式,每个元素都被立即添加到 map 中。

++的第二种方式是:

map1 ++ map2.map { case (k,v) => k -> (v + map1.getOrElse(k,0)) }

与第一种方法不同,在第二种方法中,对于第二个映射中的每个元素,将创建一个新 List 并连接到前一个映射。

case表达式使用 unapply方法隐式创建一个新 List。

Scala 2.13开始,另一个仅基于标准库的解决方案是用 groupMapReduce代替解决方案的 groupBy部分,groupMapReduce(顾名思义)相当于 groupBy后面跟着 mapValues,还有一个简化步骤:

// val map1 = Map(1 -> 9, 2 -> 20)
// val map2 = Map(1 -> 100, 3 -> 300)
(map1.toSeq ++ map2).groupMapReduce(_._1)(_._2)(_+_)
// Map[Int,Int] = Map(2 -> 20, 1 -> 109, 3 -> 300)

这个:

  • 将两个映射连接为元组序列(List((1,9), (2,20), (1,100), (3,300)))。为了简洁,map2是由 毫无疑问转换为 Seq以适应 map1.toSeq的类型-但是你可以选择使用 map2.toSeq使其明确,

  • 基于其第一个元组部分(小组MapReduce 的组部分)的 group元素,

  • map将值分组到它们的第二个元组部分(group地图Reduce 的映射部分) ,

  • reduce通过求和映射值(_+_)(groupMap减速的 reduce 部分)。

以下是我最后使用的方法:

(a.toSeq ++ b.toSeq).groupBy(_._1).mapValues(_.map(_._2).sum)

使用类型类模式,我们可以合并任何数值类型:

object MapSyntax {
implicit class MapOps[A, B](a: Map[A, B]) {
def plus(b: Map[A, B])(implicit num: Numeric[B]): Map[A, B] = {
b ++ a.map { case (key, value) => key -> num.plus(value, b.getOrElse(key, num.zero)) }
}
}
}

用法:

import MapSyntax.MapOps


map1 plus map2

合并一系列地图:

maps.reduce(_ plus _)

对于遇到 AnyVal 错误的任何人,按如下方式转换值。

错误: “无法找到参数 num 的隐式值: Numeric [ AnyVal ]”

(m1.toSeq ++ m2.toSeq).groupBy(_._1).mapValues(_.map(_._2.asInstanceOf[Number].intValue()).sum)