可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
val map1 = Map(1 -> 9 , 2 -> 20) val map2 = Map(1 -> 100, 3 -> 300)
I want to merge them, and sum the values of same keys. So the result will be:
Map(2->20, 1->109, 3->300)
Now I have 2 solutions:
val list = map1.toList ++ map2.toList val merged = list.groupBy ( _._1) .map { case (k,v) => k -> v.map(_._2).sum }
and
val merged = (map1 /: map2) { case (map, (k,v)) => map + ( k -> (v + map.getOrElse(k, 0)) ) }
But I want to know if there are any better solutions.
回答1:
Scalaz has the concept of a Semigroup which captures what you want to do here, and leads to arguably the shortest/cleanest solution:
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)
Specifically, the binary operator for Map[K, V]
combines the keys of the maps, folding V
's semigroup operator over any duplicate values. The standard semigroup for Int
uses the addition operator, so you get the sum of values for each duplicate key.
Edit: A little more detail, as per user482745's request.
Mathematically a semigroup is just a set of values, together with an operator that takes two values from that set, and produces another value from that set. So integers under addition are a semigroup, for example - the +
operator combines two ints to make another int.
You can also define a semigroup over the set of "all maps with a given key type and value type", so long as you can come up with some operation that combines two maps to produce a new one which is somehow the combination of the two inputs.
If there are no keys that appear in both maps, this is trivial. If the same key exists in both maps, then we need to combine the two values that the key maps to. Hmm, haven't we just described an operator which combines two entities of the same type? This is why in Scalaz a semigroup for Map[K, V]
exists if and only if a Semigroup for V
exists - V
's semigroup is used to combine the values from two maps which are assigned to the same key.
So because Int
is the value type here, the "collision" on the 1
key is resolved by integer addition of the two mapped values (as that's what Int's semigroup operator does), hence 100 + 9
. If the values had been Strings, a collision would have resulted in string concatenation of the two mapped values (again, because that's what the semigroup operator for String does).
(And interestingly, because string concatenation is not commutative - that is, "a" + "b" != "b" + "a"
- the resulting semigroup operation isn't either. So map1 |+| map2
is different from map2 |+| map1
in the String case, but not in the Int case.)
回答2:
The shortest answer I know of that uses only the standard library is
map1 ++ map2.map{ case (k,v) => k -> (v + map1.getOrElse(k,0)) }
回答3:
Quick solution:
(map1.keySet ++ map2.keySet).map {i=> (i,map1.getOrElse(i,0) + map2.getOrElse(i,0))}.toMap
回答4:
Well, now in scala library (at least in 2.10) there is something you wanted - merged function. BUT it's presented only in HashMap not in Map. It's somewhat confusing. Also the signature is cumbersome - can't imagine why I'd need a key twice and when I'd need to produce a pair with another key. But nevertheless, it works and much cleaner than previous "native" solutions.
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) })
Also in scaladoc mentioned that
The merged
method is on average more performant than doing a traversal and reconstructing a new immutable hash map from scratch, or ++
.
回答5:
This can be implemented as a Monoid with just plain Scala. Here is a sample implementation. With this approach, we can merge not just 2, but a list of maps.
// Monoid trait trait Monoid[M] { def zero: M def op(a: M, b: M): M }
The Map based implementation of the Monoid trait that merges two maps.
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 }
Now, if you have a list of maps that needs to be merged (in this case, only 2), it can be done like below.
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:
I wrote a blog post about this , check it out :
http://www.nimrodstech.com/scala-map-merge/
basically using scalaz semi group you can achieve this pretty easily
would look something like :
import scalaz.Scalaz._ map1 |+| map2
回答7:
map1 ++ ( for ( (k,v) ( v + map1.getOrElse(k,0) ) ) )
回答8:
Andrzej Doyle's answer contains a great explanation of semigroups which allows you to use the |+|
operator to join two maps and sum the values for matching keys.
There are many ways something can be defined to be an instance of a typeclass, and unlike the OP you might not want to sum your keys specifically. Or, you might want to do operate on a union rather than an intersection. Scalaz also adds extra functions to Map
for this purpose:
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
You can do
import scalaz.Scalaz._ map1 |+| map2 // As per other answers map1.intersectWith(map2)(_ + _) // Do things other than sum the values
回答9:
This is what I came up with...
def mergeMap(m1: Map[Char, Int], m2: Map[Char, Int]): Map[Char, Int] = { var map : Map[Char, Int] = Map[Char, Int]() ++ m1 for(p (p._2 + map.getOrElse(p._1,0))) } map }
回答10:
You can also do that with 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)
回答11:
I've got a small function to do the job, it's in my small library for some frequently used functionality which isn't in standard lib. It should work for all types of maps, mutable and immutable, not only HashMaps
Here is the usage
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
And here's the body
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
回答12:
The fastest and simplest way:
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))) )
By this way, each of element's immediately added to map.
The second ++
way is:
map1 ++ map2.map { case (k,v) => k -> (v + map1.getOrElse(k,0)) }
Unlike the first way, In a second way for each element in a second map a new List will be created and concatenated to the previous map.
The case
expression implicitly creates a new List using unapply
method.