合并两个地图并将相同键值相加的最佳方法是什么?

时间:2011-08-16 09:29:20

标签: scala map merge

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)) )
}

但我想知道是否有更好的解决方案。

15 个答案:

答案 0 :(得分:138)

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

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的标准半群使用加法运算符,因此您可以获得每个重复键的值的总和。

修改:根据user482745的要求提供更多细节。

数学上semigroup只是一组值,还有一个从该集合中获取两个值的运算符,并从该集合中生成另一个值。因此,例如,加法下的整数是半群 - +运算符组合两个整数来生成另一个整数。

你也可以在“具有给定键类型和值类型的所有地图”的集合上定义一个半群,只要你能想出一些结合了两个地图的操作来生成一个新的地图,这就是某种组合这两个输入。

如果两张地图中都没有显示任何键,则这是微不足道的。如果两个映射中都存在相同的键,那么我们需要组合键映射到的两个值。嗯,我们还没有描述一个结合了两个相同类型实体的运算符吗?这就是为什么在Scalaz中Map[K, V]的半群存在,当且仅当V的半群存在时 - V的半群用于组合分配给两个映射的两个映射的值。同样的关键。

因为Int是这里的值类型,1键上的“碰撞”通过整数加两个映射值来解析(因为这是Int的半群运算符所做的),因此{ {1}}。如果值是字符串,则碰撞会导致两个映射值的字符串连接(同样,因为这就是String的半群运算符所做的那样)。

(有趣的是,因为字符串连接不是可交换的 - 即100 + 9 - 所得到的半群操作也不是。所以"a" + "b" != "b" + "a"与String案例中的map1 |+| map2不同,但不是在Int案例中。)

答案 1 :(得分:137)

我所知道的仅使用标准库的最短答案是

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

答案 2 :(得分:46)

快速解决方案:

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

答案 3 :(得分:38)

好吧,现在在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方法平均比做一个方法更高效   从中遍历和重构一个新的不可变哈希映射   划伤,或++

答案 4 :(得分:13)

这可以通过简单的Scala实现为Monoid。这是一个示例实现。通过这种方法,我们不仅可以合并2,还可以合并地图列表。

// Monoid trait

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

合并两张地图的Monoid特征的基于地图的实现。

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)

答案 5 :(得分:5)

map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )

答案 6 :(得分:5)

我写了一篇关于此的博文,请查看:

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

基本上使用scalaz semi group你可以轻松实现这个目标

看起来像是:

  import scalaz.Scalaz._
  map1 |+| map2

答案 7 :(得分:3)

您也可以使用Cats

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)

答案 8 :(得分:2)

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.MapFunctions

你可以做到

import scalaz.Scalaz._

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

答案 9 :(得分:2)

最快最简单的方式:

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)))
)

通过这种方式,每个元素都会立即添加到地图中。

第二种++方式是:

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

与第一种方式不同,对于第二种地图中的每个元素,第二种方式是创建一个新的List并连接到上一个地图。

case表达式使用unapply方法隐式创建新列表。

答案 10 :(得分:2)

Scala 2.13开始,另一种仅基于标准库的解决方案是用groupMapReduce替换解决方案的groupBy部分(顾名思义,该部分等同于{ {1}}后跟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的类型-但您可以选择使用{{1}使其明确},

  • map1.toSeq的元素基于它们的第一个元组部分( MapReduce的组部分)

  • map2.toSeq的值分为第二个元组部分(组 Map Reduce的映射部分)

  • 通过对
  • group的映射值(map)求和(减少groupMap Reduce 的一部分)。

答案 11 :(得分:1)

这就是我想出来的......

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
}

答案 12 :(得分:1)

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

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 _)

答案 13 :(得分:0)

我有一个小功能来完成这项工作,它在我的小型库中用于一些常用的功能,这些功能在标准库中并不存在。 它应该适用于所有类型的地图,可变和不可变,而不仅仅是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

答案 14 :(得分:0)

这就是我最终使用的内容:

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