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.
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
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)) }
Solution rapide:
(map1.keySet ++ map2.keySet).map {i=> (i,map1.getOrElse(i,0) + map2.getOrElse(i,0))}.toMap
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++
.
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)
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
map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )
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)
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:
Tu peux faire
import scalaz.Scalaz._
map1 |+| map2 // As per other answers
map1.intersectWith(map2)(_ + _) // Do things other than sum the values
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
}
À 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))
)
group
s éléments basés sur leur première partie Tuple (partie groupe de groupe MapReduce)
map
s ont regroupé les valeurs dans leur deuxième partie Tuple (partie de la carte du groupeCarte Réduire)
reduce
s valeurs mappées (_+_
) en les additionnant (réduction d'une partie de groupMapReduce)
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()
}
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
.
Voici ce que j'ai fini par utiliser:
(a.toSeq ++ b.toSeq).groupBy(_._1).mapValues(_.map(_._2).sum)