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


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

我想合并它们,并对相同键的值求和。因此结果将是:

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

现在我有2个解决方案:

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

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


最容易的是map1 ++ map2
Seraf

3
@Seraf实际上只是“合并”地图,而忽略重复项而不是求和它们的值。
Zeynep Akkalyoncu Yilmaz

@ZeynepAkkalyoncuYilmaz正确的应该更
清楚

Answers:


142

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的要求,提供更多细节。

从数学上讲,半群只是一组值,以及一个运算符,该运算符从该集中获取两个值,并从该集中产生另一个值。因此,加法+运算中的整数是一个半群,例如- 运算符将两个int合并为另一个int。

您还可以在“具有给定键类型和值类型的所有映射”的集合上定义一个半组,只要您能提出一些操作,将两个映射组合在一起以生成一个新映射,这就是两者的组合输入。

如果两个地图中都没有按键,这是微不足道的。如果两个映射中都存在相同的键,那么我们需要组合键映射到的两个值。嗯,我们不是刚刚描述了一个将两个相同类型的实体组合在一起的运算符吗?这就是为什么在Scalaz一个半群Map[K, V]当且仅当一个半群的存在,V存在- V的半群用于将值从被分配到相同的密钥两张地图结合起来。

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

(有趣的是,因为字符串连接是不可交换的-那就是"a" + "b" != "b" + "a"-生成的半群操作也不那么,map1 |+| map2是从不同map2 |+| map1的字符串的情况下,而不是在诠释情况。)


37
辉煌!第一个实际的例子很scalaz有道理。
SOC

5
别开玩笑了!如果您开始寻找它...到处都是。引用规范和规范2的作者erric torrebone的话:“首先,您学习Option,然后开始在各处看到它。然后,您学习了Applicative,这是一回事。接下来吗?” 接下来是更多的功能概念。这些极大地帮助您构造代码并很好地解决问题。
AndreasScheinert 2011年

4
实际上,当我终于找到Scala时,我一直在寻找Option 5年。可能为null 的Java对象引用与不能为null 的Java对象引用(即A和之间Option[A])之间的差异是如此之大,我无法相信它们确实是同一类型。我开始看着Scalaz。我不确定我是否足够聪明……
Malvolio

1
Java也有Option,请参见Functional Java。不用担心,学习很有趣。函数式编程不会(只)教您新事物,而可以为程序员提供帮助,帮助他们提供解决问题的术语和词汇。OP问题就是一个很好的例子。Semigroup的概念非常简单,您每天都会用它来表示字符串。如果您确定了此抽象,将其命名,然后将其最终应用于其他类型,然后将其应用于String,则将显示真正的力量。
2011年

1
它怎么可能导致1->(100 + 9)?你能给我看看“堆栈跟踪”吗?谢谢。PS:我想在这里让答案更清楚。
user482745'2

151

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

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

34
不错的解决方案。我想添加一个提示,如果左侧已经存在(k,_),则++用(k,v)替换右侧地图中++(map1)左侧地图中的任何(k,v)侧面图(此处为map1),例如Map(1->1) ++ Map(1->2) results in Map(1->2)
Lutz,

一种更整洁的版本:对于((k,v)<-(aa ++ bb))产生k->(如果(((aa包含k)&&(bb包含k))aa(k)+ v else v)
2014年

我以前做过一些不同的事情,但这是您所做的工作的一个版本,将地图替换为formap1 ++(对于((k,v)<-map2)yield k->(v + map1.getOrElse(k,0 )))
2014年

1
@ Jus12-No. .具有更高的优先级++;你读map1 ++ map2.map{...}map1 ++ (map2 map {...})。因此,一种映射map1元素的方式,另一种则不。
Rex Kerr 2015年

1
@matt-Scalaz已经做到了,所以我要说“现有的库已经做到了”。
雷克斯·克尔


41

好吧,现在在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与进行遍历并从头开始构造新的不可变哈希图相比,该方法的平均性能更高++


1
截至目前,它仅在不可变的Hashmap中,而不在可变的Hashmap中。
凯文·惠勒

2
老实说,他们只有HashMaps拥有这种功能。
Johan S

我无法对此进行编译,似乎它接受的类型是私有的,因此我无法传递匹配的类型化函数。
Ryan The Leach 2015年

2
在2.11版本中似乎有所更改。检查出2.10 scaladoc - scala-lang.org/api/2.10.1/...有一个常用的功能。但在2.11中是MergeFunction
Mikhail Golubtsov 2015年

所有这一切在2.11已经改变了这种特别的功能类型引入了类型别名private type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1)
EthanP

14

可以将其实现为仅具有普通Scala 的Monoid。这是一个示例实现。使用这种方法,我们不仅可以合并2个地图,还可以合并一个地图列表。

// Monoid trait

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

Monoid特征的基于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)

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

5

我写了一篇关于此的博客文章,查看一下:

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

基本上使用scalaz semi group,您可以轻松实现

看起来像:

  import scalaz.Scalaz._
  map1 |+| map2

11
您需要在答案中添加更多细节,最好是一些实现代码。对您发布的其他类似答案也要执行此操作,并针对所提出的特定问题调整每个答案。 经验法则:提问者应该能够从您的答案中受益,而无需单击博客链接。
罗伯特·哈维

5

您也可以使用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)

伊克,import cats.implicits._。导入import cats.instances.map._ import cats.instances.int._ import cats.syntax.semigroup._更多详细信息...
St.Antario '18

@ St.Antario实际上是推荐的方式import cats.implicits._
Artsiom Miklushou

推荐给谁?将所有(大多数未使用的)隐式实例带入范围会使编译器的工作变得复杂。而且,如果不需要,比如说应用实例,为什么他们会把它带到那里呢?
圣安东尼奥

4

开始时Scala 2.13,另一个仅基于标准库的解决方案包括替换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

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

  • map将值分组到第二个元组部分(Map Reduce 组的映射部分),

  • reduce_+_通过对它们的映射值()求和(减少groupMap Reduce的一部分)。


3

这是我最终使用的内容:

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

1
这与OP提出的第一种解决方案确实没有太大区别。
jwvh

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

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表达式使用unapplymethod 隐式创建一个新的List 。


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
}

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.