合并两个地图并对相同键的值求和的最佳方法?
Posted
技术标签:
【中文标题】合并两个地图并对相同键的值求和的最佳方法?【英文标题】:Best way to merge two maps and sum the values of same key? 【发布时间】:2011-10-27 21:57:41 【问题描述】: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 这实际上只是简单地“合并”了地图,忽略重复而不是求和它们的值。
@ZeynepAkkalyoncuYilmaz 应该更好地阅读这个问题,羞愧地离开
【参考方案1】:
我所知道的仅使用标准库的最短答案是
map1 ++ map2.map case (k,v) => k -> (v + map1.getOrElse(k,0))
【讨论】:
不错的解决方案。我喜欢添加提示,++
将 ++
左侧地图中的任何 (k,v) 替换为右侧地图中的 (k,v),如果 (k,_ ) 已存在于左侧地图(此处为 map1)中,例如Map(1->1) ++ Map(1->2) results in Map(1->2)
一种更简洁的版本:for ((k, v) (if ((aa contains k) && (bb contains k)) aa (k) + v 否则 v)
我之前做了一些不同的事情,但这是你所做的一个版本,将地图替换为 for
map1 ++ (for ((k,v) (v + map1.getOrElse(k,0)))
@Jus12 - 否。.
的优先级高于 ++
;你把map1 ++ map2.map...
读作map1 ++ (map2 map ...)
。因此,一种方式是映射 map1
s 元素,另一种方式是不映射。
@matt - Scalaz 已经可以做到,所以我会说“现有的库已经做到了”。【参考方案2】:
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 中,当且仅当存在 V
的半群时,才会存在 Map[K, V]
的半群 - V
的半群用于组合分配给同一键的两个映射中的值。
所以因为Int
是这里的值类型,1
键上的“冲突”通过两个映射值的整数相加来解决(这就是 Int 的半群运算符所做的),因此是 100 + 9
。如果值是字符串,则冲突将导致两个映射值的字符串连接(同样,因为这是字符串的半群运算符所做的)。
(有趣的是,因为字符串连接不是可交换的 - 即"a" + "b" != "b" + "a"
- 生成的半群运算也不是。所以map1 |+| map2
与 map2 |+| map1
在 String 情况下不同,但在 Int 情况下则不同情况。)
【讨论】:
太棒了!scalaz
有意义的第一个实际示例。
不开玩笑!如果你开始寻找它......它到处都是。引用 specs 和 specs2 的作者 erric torrebone 的话:“首先你学习 Option,然后你开始到处看到它。然后你学习 Applicative,它是同一件事。下一步?”接下来是更多的功能概念。这些极大地帮助您构建代码并很好地解决问题。
实际上,当我终于找到 Scala 时,我一直在寻找 Option 五年。 可能为空的Java对象引用和不能为空的Java对象引用(即A
和Option[A]
之间)之间的差异是如此之大,我不敢相信它们真的是同一类型。我刚刚开始关注 Scalaz。我不确定我是否足够聪明......
Java 也有选项,请参阅函数式 Java。不要有任何恐惧,学习很有趣。函数式编程不会(仅)教你新东西,而是为你提供程序员帮助,提供术语、词汇来解决问题。 OP 问题就是一个很好的例子。半群的概念非常简单,您每天都使用它,例如字符串。如果你识别出这个抽象,命名它并最终将它应用于其他类型,那么真正的力量就会出现,然后只是 String。
它怎么可能导致 1 -> (100 + 9) ?你能告诉我“堆栈跟踪”吗?谢谢。 P.S.:我在这里要求让答案更清楚。【参考方案3】:
快速解决方案:
(map1.keySet ++ map2.keySet).map i=> (i,map1.getOrElse(i,0) + map2.getOrElse(i,0)).toMap
【讨论】:
【参考方案4】:好吧,现在在 scala 库中(至少在 2.10 中)有你想要的东西 - merged 函数。但它只出现在 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
方法的平均性能比执行 遍历并重建一个新的不可变哈希映射 从头开始,或++
。
【讨论】:
目前,它只存在于不可变的Hashmap中,而不是可变的Hashmap。 这很烦人,老实说他们只有 HashMaps。 我无法编译它,它接受的类型似乎是私有的,所以我无法传入匹配的类型函数。 似乎在 2.11 版本中有所改变。查看 2.10 scaladoc - scala-lang.org/api/2.10.1/… 有一个常用功能。但在 2.11 中,它是MergeFunction
。
所有在 2.11 中改变的是为这个特定的函数类型引入了一个类型别名private type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1)
【参考方案5】:
这可以通过简单的 Scala 实现为 Monoid。这是一个示例实现。通过这种方法,我们不仅可以合并 2 个地图,还可以合并地图列表。
// Monoid trait
trait Monoid[M]
def zero: M
def op(a: M, b: M): M
基于 Map 的 Monoid trait 实现,它合并了两个地图。
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)
【讨论】:
【参考方案6】:map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )
【讨论】:
【参考方案7】:我写了一篇关于此的博客文章,请查看:
http://www.nimrodstech.com/scala-map-merge/
基本上使用 scalaz semi 组你可以很容易地做到这一点
看起来像:
import scalaz.Scalaz._
map1 |+| map2
【讨论】:
您需要在答案中添加更多细节,最好是一些实现代码。对您发布的其他类似答案也这样做,并针对所提出的特定问题定制每个答案。 经验法则:提问者应该能够从您的回答中受益,而无需点击博客链接。【参考方案8】:您也可以使用 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 实际上建议只有import cats.implicits._
谁推荐的?将所有(其中大部分是未使用的)隐式实例带入作用域会使编译器的生命复杂化。此外,如果不需要,比如说,Applicative 实例,他们为什么要把它带到那里?【参考方案9】:
从Scala 2.13
开始,另一种仅基于标准库的解决方案是将解决方案的groupBy
部分替换为groupMapReduce
(顾名思义)相当于groupBy
,后跟@987654326 @ 和一个减少步骤:
// 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
s 元素基于它们的第一个元组部分(groupMapReduce 的组部分),
map
s 将值分组到它们的第二个元组部分(组的映射部分MapReduce),
reduce
s 映射值 (_+_
) 求和(减少 groupMap 的一部分Reduce)。
【讨论】:
【参考方案10】:这是我最终使用的:
(a.toSeq ++ b.toSeq).groupBy(_._1).mapValues(_.map(_._2).sum)
【讨论】:
这与 OP 提出的第一个解决方案并没有太大区别。【参考方案11】: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
【讨论】:
【参考方案12】:最快最简单的方法:
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))
与第一种方式不同,在第二种方式中,将为第二个映射中的每个元素创建一个新列表并将其连接到前一个映射。
case
表达式使用unapply
方法隐式创建一个新列表。
【讨论】:
【参考方案13】:这就是我想出的......
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
【讨论】:
【参考方案14】:使用 typeclass 模式,我们可以合并任何 Numeric 类型:
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 _)
【讨论】:
【参考方案15】:我有一个小函数来完成这项工作,它在我的小库中,用于一些标准库中没有的常用功能。 它应该适用于所有类型的映射,可变的和不可变的,不仅是 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
【讨论】:
【参考方案16】:对于遇到 AnyVal 错误的任何人,请按如下方式转换值。
错误: “找不到参数 num 的隐式值:Numeric[AnyVal]”
(m1.toSeq ++ m2.toSeq).groupBy(_._1).mapValues(_.map(_._2.asInstanceOf[Number].intValue()).sum)
【讨论】:
使用asInstanceOf
是bad practice,应该避免使用。
会牢记这一点。用 scala 工作了大约一个星期,这让我的代码工作了大声笑以上是关于合并两个地图并对相同键的值求和的最佳方法?的主要内容,如果未能解决你的问题,请参考以下文章
在多重映射中,当两个迭代器保存具有映射到不同 Value 的相同键的值时。我们如何才能在地图中找到它们中的哪一个在另一个之前