web-dev-qa-db-fra.com

Le meilleur moyen de fusionner deux cartes et d’additionner les valeurs de la même clé?

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

Je veux les fusionner et additionner les valeurs des mêmes clés. Donc, le résultat sera:

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

Maintenant, j'ai 2 solutions:

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

et

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

Mais je veux savoir s'il existe de meilleures solutions.

154
Freewind

Scalaz a le concept de Semigroup qui capture ce que vous voulez faire ici et mène à la solution la plus courte/la plus propre:

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)

Spécifiquement, l'opérateur binaire pour Map[K, V] combine les clés des cartes, en repliant l'opérateur semigroupe de V sur toutes les valeurs en double. Le semigroupe standard de Int utilise l'opérateur d'addition. Vous obtenez ainsi la somme des valeurs pour chaque clé dupliquée.

Edit: Un peu plus en détail, à la demande de l'utilisateur482745.

Mathématiquement, un semigroupe n'est qu'un ensemble de valeurs, associé à un opérateur qui extrait deux valeurs de cet ensemble et en génère une autre. Ainsi, les entiers en addition sont un semi-groupe, par exemple - l'opérateur + combine deux entiers pour en faire un autre int.

Vous pouvez également définir un semi-groupe sur l'ensemble de "toutes les cartes avec un type de clé et un type de valeur donnés", à condition de pouvoir réaliser une opération combinant deux cartes pour en produire une nouvelle, qui est en quelque sorte la combinaison des deux. contributions.

Si aucune clé n'apparaît dans les deux cartes, c'est trivial. Si la même clé existe dans les deux cartes, nous devons combiner les deux valeurs associées à la clé. Hmm, ne venons-nous pas de décrire un opérateur qui combine deux entités du même type? C'est pourquoi, dans Scalaz, un semigroupe pour Map[K, V] existe si et seulement si un semigroupe pour V existe - le semigroupe de V est utilisé pour combiner les valeurs de deux cartes affectées à la même clé.

Donc, puisque Int est le type de valeur ici, la "collision" sur la clé 1 est résolue par l'addition entière des deux valeurs mappées (comme c'est ce que fait l'opérateur de semi-groupe d'Int), d'où 100 + 9. Si les valeurs avaient été Strings, une collision aurait abouti à la concaténation des chaînes des deux valeurs mappées (encore une fois, car c'est ce que fait l'opérateur semigroup pour String).

(Et il est intéressant de noter que la concaténation de chaînes n'étant pas commutative - c'est-à-dire "a" + "b" != "b" + "a" - l'opération de semigroupe résultante ne l'est pas non plus. Donc map1 |+| map2 est différent de map2 |+| map1 dans le cas de la chaîne, mais pas dans celui de l'int

138
Andrzej Doyle

La réponse la plus courte que je connaisse qui utilise uniquement la bibliothèque standard est

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

Solution rapide:

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

Eh bien, maintenant dans la bibliothèque scala (au moins dans la version 2.10), il y a quelque chose que vous vouliez - fusionné fonction. MAIS il est présenté uniquement dans HashMap et non dans Map. C'est un peu déroutant. De plus, la signature est encombrante - je ne vois pas pourquoi j'aurais besoin d'une clé deux fois et quand il me faudrait produire une paire avec une autre clé. Mais néanmoins, cela fonctionne et beaucoup plus propre que les solutions "natives" précédentes.

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

Aussi dans scaladoc a mentionné que

La méthode merged est en moyenne plus performante que de faire un traversant et reconstruisant une nouvelle carte de hachage immuable à partir de scratch, ou ++.

37
Mikhail Golubtsov

Ceci peut être implémenté en tant que Monoid avec tout simplement Scala. Voici un exemple de mise en œuvre. Avec cette approche, nous pouvons fusionner non seulement 2, mais une liste de cartes.

// Monoid trait

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

Implémentation basée sur la carte du trait Monoïd qui fusionne deux cartes.

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
}

Maintenant, si vous avez une liste de cartes à fusionner (dans ce cas, seulement 2), vous pouvez procéder comme ci-dessous.

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

J'ai écrit un billet de blog à ce sujet, allez voir:

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

essentiellement en utilisant le groupe semi scalaz, vous pouvez y arriver assez facilement

ressemblerait à quelque chose comme:

  import scalaz.Scalaz._
  map1 |+| map2
5
Nimrod007
map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )
5
AmigoNico

Vous pouvez également le faire avec 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)
3
Artsiom Miklushou

La réponse d'Andrzej Doyle contient une excellente explication des semi-groupes qui vous permet d'utiliser l'opérateur |+| pour joindre deux cartes et additionner les valeurs des clés correspondantes.

Il existe de nombreuses façons de définir une instance d'une classe de types et, contrairement au PO, vous ne souhaitez pas forcément additionner vos clés. Ou, vous voudrez peut-être opérer sur un syndicat plutôt que sur une intersection. Scalaz ajoute également des fonctions supplémentaires à Map à cette fin:

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

Tu peux faire

import scalaz.Scalaz._

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

C'est ce que je suis venu avec ...

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
kaur

À partir de Scala 2.13, une autre solution basée uniquement sur la bibliothèque standard consiste à remplacer la partie groupBy de votre solution par groupMapReduce qui (comme son nom l'indique) est l'équivalent d'une groupBy suivie de mapValues et d'une étape de réduction:

// val map1 = Map(1 -> 9, 2 -> 20)
// val map2 = Map(1 -> 100, 3 -> 300)
(map1.toSeq ++ map2.toSeq).groupMapReduce(_._1)(_._2)(_+_)
// Map[Int,Int] = Map(2 -> 20, 1 -> 109, 3 -> 300)

Ce:

  • concatène les deux cartes en une séquence de n-uplets (List((1,9), (2,20), (1,100), (3,300)))

  • groups éléments basés sur leur première partie Tuple (partie groupe de groupe MapReduce)

  • maps ont regroupé les valeurs dans leur deuxième partie Tuple (partie de la carte du groupeCarte Réduire)

  • reduces valeurs mappées (_+_) en les additionnant (réduction d'une partie de groupMapReduce)

0
Xavier Guihot

J'ai une petite fonction pour faire le travail, elle se trouve dans ma petite bibliothèque pour certaines fonctionnalités fréquemment utilisées qui ne figurent pas dans la bibliothèque standard .. .. Elle devrait fonctionner pour tous les types de cartes, modifiables et immuables, et pas seulement pour HashMaps.

Voici l'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

Et voici le corps

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.2Fpackage.scala#L190

0
Eugene Platonov

Le moyen le plus rapide et le plus simple:

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 cette façon, chaque élément est immédiatement ajouté à la carte.

La deuxième manière ++ est:

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

Contrairement à la première manière, une nouvelle liste sera créée pour être ensuite concaténée avec la carte précédente.

L'expression case crée implicitement une nouvelle liste à l'aide de la méthode unapply.

0
Alexey Kudryashov

Voici ce que j'ai fini par utiliser:

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