¿La mejor manera de combinar dos mapas y sumar los valores de la misma clave?


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

Quiero fusionarlos y sumar los valores de las mismas claves. Así que el resultado será:

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

Ahora tengo 2 soluciones:

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

Y

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

Pero quiero saber si hay mejores soluciones.

Author: Freewind, 2011-08-16

12 answers

Scalaz tiene el concepto de un Semigrupo que captura lo que quieres hacer aquí, y conduce a la solución más corta/limpia:

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)

Específicamente, el operador binario para Map[K, V] combina las teclas de los mapas, doblando el operador de semigrupo de V sobre cualquier valor duplicado. El semigrupo estándar para Int utiliza el operador de suma, por lo que obtiene la suma de valores para cada clave duplicada.

Editar: Un poco más de detalle, según solicitud del usuario 482745.

Matemáticamente un semigrupo es solo un conjunto de valores, junto con un operador que toma dos valores de ese conjunto, y produce otro valor de ese conjunto. Así que los enteros bajo suma son un semigrupo, por ejemplo-el operador + combina dos int para hacer otro int.

También puede definir un semigrupo sobre el conjunto de "todos los mapas con un tipo de clave y tipo de valor dado", siempre que pueda idear alguna operación que combine dos mapas para producir uno nuevo que es de alguna manera la combinación de las dos entradas.

Si no hay claves que aparezcan en ambos mapas, esto es trivial. Si la misma clave existe en ambos mapas, entonces necesitamos combinar los dos valores a los que se asigna la clave. Hmm, ¿no acabamos de describir un operador que combina dos entidades del mismo tipo? Esta es la razón por la que en Scalaz existe un semigroup para Map[K, V] si y solo si existe un Semigroup para V - el semigroup de V se usa para combinar los valores de dos mapas que se asignan a la misma clave.

Entonces, como Int es el tipo de valor aquí, la "colisión" en la clave 1 se resuelve mediante la adición de enteros de los dos valores asignados (como lo hace el operador de semigrupo de Int), por lo tanto 100 + 9. Si los valores hubieran sido Cadenas, una colisión habría resultado en la concatenación de cadenas de los dos valores asignados (de nuevo, porque eso es lo que hace el operador de semigrupo para Cadena).

(Y curiosamente, porque la concatenación de cadenas no es conmutativa-es decir, "a" + "b" != "b" + "a" - la operación de semigrupo resultante tampoco lo es. Así que map1 |+| map2 es diferente de map2 |+| map1 en el caso String, pero no en el caso Int.)

 132
Author: Andrzej Doyle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-04-11 02:39:33

La respuesta más corta que conozco que solo usa la biblioteca estándar es

map1 ++ map2.map{ case (k,v) => k -> (v + map1.getOrElse(k,0)) }
 127
Author: Rex Kerr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-08-16 14:58:35

Solución rápida:

(map1.keySet ++ map2.keySet).map {i=> (i,map1.getOrElse(i,0) + map2.getOrElse(i,0))}.toMap
 41
Author: Matthew Farwell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-08-23 13:23:16

Bueno, ahora en la biblioteca scala (al menos en la 2.10) hay algo que querías - fusionado función. PERO se presenta solo en HashMap, no en Map. Es algo confuso. También la firma es engorrosa - no puedo imaginar por qué necesitaría una clave dos veces y cuando tendría que producir un par con otra clave. Pero sin embargo, funciona y mucho más limpio que las soluciones "nativas" anteriores.

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

También en scaladoc mencionó que

El método merged es en promedio más performant than doing a atravesar y reconstruir un nuevo mapa hash inmutable desde scratch, o ++.

 37
Author: Mikhail Golubtsov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-07-06 21:33:02

Esto se puede implementar como un Monoide con Scala simple. Aquí hay una implementación de ejemplo. Con este enfoque, podemos combinar no solo 2, sino una lista de mapas.

// Monoid trait

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

La implementación basada en mapas del rasgo Monoide que fusiona dos mapas.

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
}

Ahora, si tiene una lista de mapas que necesita fusionarse (en este caso, solo 2), se puede hacer como a continuación.

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)
 12
Author: Jegan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-10 13:39:49
map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )
 5
Author: AmigoNico,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-01-06 09:19:12

Escribí una entrada de blog sobre esto, échale un vistazo:

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

Básicamente usando scalaz semi group puedes lograr esto bastante fácilmente

Se vería algo así como:

  import scalaz.Scalaz._
  map1 |+| map2
 5
Author: Nimrod007,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-29 13:44:59

También puedes hacer eso con Gatos.

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)
 2
Author: Artsiom Miklushou,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-06 20:45:25

La respuesta de Andrzej Doyle contiene una gran explicación de los semigrupos que le permite usar el operador |+| para unir dos mapas y sumar los valores de las claves coincidentes.

Hay muchas maneras en que algo se puede definir como una instancia de una clase de tipo, y a diferencia del OP, es posible que no desee sumar sus claves específicamente. O, es posible que desee operar en un sindicato en lugar de una intersección. Scalaz también agrega funciones adicionales a Map para esto propósito:

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

Puedes hacer

import scalaz.Scalaz._

map1 |+| map2 // As per other answers
map1.intersectWith(map2)(_ + _) // Do things other than sum the values
 2
Author: user1158559,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-08-08 10:36:01

Esto es lo que se me ocurrió...

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
Author: kaur,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-23 20:12:11

Tengo una pequeña función para hacer el trabajo, está en mi pequeña biblioteca para alguna funcionalidad de uso frecuente que no está en la librería estándar. Debería funcionar para todo tipo de mapas, mutables e inmutables, no solo HashMaps

Aquí está el uso

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

Y aquí está el cuerpo

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

 0
Author: Eugene Platonov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-15 21:17:00

La forma más rápida y sencilla:

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

De esta manera, cada uno de los elementos se agrega inmediatamente al mapa.

El segundo camino ++ es:

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

A diferencia de la primera manera, de una segunda manera para cada elemento en un segundo mapa se creará una nueva Lista y se concatenará con el mapa anterior.

La expresión case crea implícitamente una nueva Lista usando el método unapply.

 0
Author: Alexey Kudryashov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-01-07 20:28:59