Il modo migliore per unire due mappe e sommare i valori della stessa chiave?


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

Voglio unirli e sommare i valori delle stesse chiavi. Quindi il risultato sarà:

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

Ora ho 2 soluzioni:

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

e

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

Ma voglio sapere se ci sono soluzioni migliori.


Il più semplice èmap1 ++ map2
Seraf, il

3
@Seraf In realtà semplicemente "unisce" le mappe, ignorando i duplicati invece di sommare i loro valori.
Zeynep Akkalyoncu Yilmaz,

@ZeynepAkkalyoncuYilmaz avrebbe dovuto leggere meglio la domanda, lascia vergognarsi
Seraf

Risposte:


143

Scalaz ha il concetto di un semigruppo che cattura ciò che vuoi fare qui e porta probabilmente alla soluzione più breve / pulita:

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)

In particolare, l'operatore binario per Map[K, V]combina le chiavi delle mappe, piegando Vl'operatore del semigruppo su qualsiasi valore duplicato. Il semigruppo standard Intutilizza l'operatore addizione, in modo da ottenere la somma dei valori per ciascuna chiave duplicata.

modificare : un po 'più di dettaglio, secondo la richiesta dell'utente482745.

Matematicamente un semigruppo è solo un insieme di valori, insieme a un operatore che prende due valori da quell'insieme e produce un altro valore da quell'insieme. Quindi gli interi in aggiunta sono un semigruppo, ad esempio: l' +operatore combina due in per creare un altro int.

Puoi anche definire un semigruppo sull'insieme di "tutte le mappe con un determinato tipo di chiave e tipo di valore", a condizione che tu possa inventare qualche operazione che combini due mappe per produrne una nuova che sia in qualche modo la combinazione delle due ingressi.

Se non ci sono chiavi che appaiono in entrambe le mappe, questo è banale. Se la stessa chiave esiste in entrambe le mappe, allora dobbiamo combinare i due valori a cui è mappata la chiave. Hmm, non abbiamo appena descritto un operatore che combina due entità dello stesso tipo? Questo è il motivo per cui in Scalaz Map[K, V]esiste un semigruppo per se e solo se Vesiste un semigruppo per -V il semigruppo viene utilizzato per combinare i valori di due mappe assegnate alla stessa chiave.

Quindi, poiché qui Intè il tipo di valore, la "collisione" sulla 1chiave viene risolta mediante l'aggiunta intera dei due valori mappati (poiché è quello che fa l'operatore del semigruppo di Int), quindi100 + 9 . Se i valori fossero stati String, una collisione avrebbe comportato la concatenazione di stringhe dei due valori mappati (di nuovo, perché è quello che fa l'operatore del semigruppo per String).

(E interessante, perché la concatenazione di stringhe non è commutativa - cioè, "a" + "b" != "b" + "a"l'operazione del semigruppo risultante non è neanche. Quindi map1 |+| map2è diversa dal map2 |+| map1caso String, ma non nel caso Int.)


37
Brillante! Primo esempio pratico in cui scalazaveva senso.
soc,

5
Non scherzo! Se inizi a cercarlo ... è dappertutto. Per citare l'errico torrebone autore di specs e specs2: "Prima impari l'Opzione e inizi a vederlo ovunque. Poi impari Applicativo ed è la stessa cosa. Successivo?" I prossimi sono concetti ancora più funzionali. E quelli ti aiutano molto a strutturare il tuo codice e a risolvere bene i problemi.
AndreasScheinert,

4
In realtà, stavo cercando Opzione da cinque anni quando finalmente ho trovato Scala. La differenza tra un riferimento a un oggetto Java che potrebbe essere nullo e uno che non può essere (cioè tra Ae Option[A]) è così grande, non potevo credere che fossero davvero dello stesso tipo. Ho appena iniziato a guardare Scalaz. Non sono sicuro di essere abbastanza intelligente ...
Malvolio,

1
C'è anche un'opzione per Java, vedi Java funzionale. Non temere, l'apprendimento è divertente. E la programmazione funzionale non ti insegna nuove cose (solo) ma ti offre invece l'aiuto del programmatore nel fornire termini, vocabolario per affrontare i problemi. La domanda OP è un esempio perfetto. Il concetto di un semigruppo è così semplice che lo usi tutti i giorni, ad esempio per le stringhe. Il vero potere appare se identifichi questa astrazione, la chiami e infine la applichi ad altri tipi, quindi solo a String.
AndreasScheinert,

1
Come è possibile che si traduca in 1 -> (100 + 9)? Potete per favore mostrarmi "stack trace"? Grazie. PS: sto chiedendo qui per rendere la risposta più chiara.
user482745

152

La risposta più breve che conosco che utilizza solo la libreria standard è

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

34
Bella soluzione. Mi piace aggiungere il suggerimento, che ++sostituisce qualsiasi (k, v) dalla mappa sul lato sinistro di ++(qui map1) con (k, v) dalla mappa del lato destro, se (k, _) esiste già a sinistra mappa laterale (qui map1), ad esMap(1->1) ++ Map(1->2) results in Map(1->2)
Lutz,

Una specie di versione più ordinata: per ((k, v) <- (aa ++ bb)) resa k -> (if ((aa contiene k) && (bb contiene k)) aa (k) + v else v)
dividebyzero,

Ho fatto qualcosa di diverso in precedenza, ma ecco una versione di quello che hai fatto, sostituendo la mappa con una formap1 ++ (per ((k, v) <- map2) yield k -> (v + map1.getOrElse (k, 0 )))
dividebyzero il

1
@ Jus12 - No. .ha una precedenza più alta di ++; leggi map1 ++ map2.map{...}come map1 ++ (map2 map {...}). Quindi un modo per mappare gli map1elementi e l'altro no.
Rex Kerr,

1
@matt - Lo farà già Scalaz, quindi direi "già una libreria esistente lo fa".
Rex Kerr,

48

Soluzione rapida:

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

41

Bene, ora nella libreria scala (almeno in 2.10) c'è qualcosa che volevi: la funzione unita . MA è presentato solo in HashMap e non in Mappa. È un po 'confuso. Anche la firma è ingombrante - non riesco a immaginare perché avrei bisogno di una chiave due volte e quando dovrei produrre una coppia con un'altra chiave. Tuttavia, funziona e molto più pulito rispetto alle precedenti soluzioni "native".

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

Anche in scaladoc ha detto che

Il mergedmetodo è in media più performante rispetto a fare una traversata e ricostruire una nuova mappa di hash immutabile da zero, o ++.


1
A partire da ora, è solo in Hashmap immutabile, non in Hashmap mutabile.
Kevin Wheeler,

2
Questo è piuttosto fastidioso che hanno solo quello per HashMaps per essere onesti.
Johan S,

Non riesco a compilare questo, sembra che il tipo che accetta sia privato, quindi non riesco a passare una funzione digitata che corrisponde.
Ryan The Leach,

2
Sembra che qualcosa sia cambiato nella versione 2.11. Controlla 2.10 scaladoc - scala-lang.org/api/2.10.1/… Esiste una normale funzione. Ma in 2.11 lo è MergeFunction.
Mikhail Golubtsov,

Tutto ciò che è cambiato in 2.11 è l'introduzione di un alias di tipo per questo particolare tipo di funzioneprivate type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1)
EthanP

14

Questo può essere implementato come un Monoide con solo Scala. Ecco un'implementazione di esempio. Con questo approccio, possiamo unire non solo 2, ma un elenco di mappe.

// Monoid trait

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

L'implementazione basata sulla mappa del tratto Monoid che unisce due mappe.

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
}

Ora, se hai un elenco di mappe che devono essere unite (in questo caso, solo 2), puoi farlo come di seguito.

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)

5
map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )

5

Ho scritto un post sul blog a riguardo, dai un'occhiata:

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

fondamentalmente usando il semi-gruppo scalaz puoi farlo abbastanza facilmente

sarebbe simile a:

  import scalaz.Scalaz._
  map1 |+| map2

11
Devi aggiungere un po 'più di dettaglio nella tua risposta, preferibilmente un po' di codice di implementazione. Fallo anche per le altre risposte simili che hai pubblicato e personalizza ogni risposta alla domanda specifica che è stata posta. Regola empirica: il richiedente dovrebbe essere in grado di beneficiare della risposta senza fare clic sul collegamento al blog.
Robert Harvey,

5

Puoi farlo anche con 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)

Eek, import cats.implicits._. Importazione import cats.instances.map._ import cats.instances.int._ import cats.syntax.semigroup._non molto più dettagliata ...
Sant'Antario,

@Antario è in realtà il modo consigliato di avere soloimport cats.implicits._
Artsiom Miklushou il

Raccomandato da chi? Portare tutte le istanze implicite (la maggior parte delle quali inutilizzate) nello scope complica la vita del compilatore. E inoltre se uno non ha bisogno, per esempio, di un'istanza applicativa perché dovrebbe portarla lì?
Sant'Antario,

4

A partire Scala 2.13, un'altra soluzione basata solo sulla libreria standard consiste nel sostituire la groupByparte della soluzione con la groupMapReducequale (come suggerisce il nome) equivale a un passaggio groupByseguito da mapValuesun passaggio ridotto:

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

Questo:

  • Concatena le due mappe come una sequenza di tuple ( List((1,9), (2,20), (1,100), (3,300))). Per concisione, map2viene convertito implicitamente in Seqper adattarsi al tipo di map1.toSeq- ma potresti scegliere di renderlo esplicito utilizzando map2.toSeq,

  • groups elementi basati sulla loro prima parte tupla (parte gruppo del gruppo MapReduce),

  • maps valori raggruppati nella seconda parte di tupla (parte della mappa del gruppo Riduci mappa ),

  • reduces valori mappati ( _+_) sommandoli (riduci parte del gruppo Riduci mappa ).


3

Ecco cosa ho finito per usare:

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

1
Non è molto diverso dalla prima soluzione proposta dal PO.
jwvh,

2

La risposta di Andrzej Doyle contiene una grande spiegazione dei semigruppi che ti permette di usare il |+| operatore per unire due mappe e sommare i valori per le chiavi corrispondenti.

Esistono molti modi in cui qualcosa può essere definito come un'istanza di una typeclass e, diversamente dall'OP, potresti non voler sommare le tue chiavi in ​​modo specifico. Oppure, potresti voler operare su un'unione piuttosto che su un incrocio. A Mapquesto scopo, Scalaz aggiunge ulteriori funzioni :

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 puoi fare

import scalaz.Scalaz._

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

2

Il modo più veloce e semplice:

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

In questo modo, ciascuno degli elementi viene immediatamente aggiunto alla mappa.

Il secondo ++modo è:

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

A differenza del primo modo, in un secondo modo per ogni elemento in una seconda mappa verrà creato un nuovo Elenco e concatenato alla mappa precedente.

L' caseespressione crea implicitamente un nuovo elenco usando il unapplymetodo


1

Questo è quello che mi è venuto in mente ...

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

Usando il modello di typeclass, possiamo unire qualsiasi tipo numerico:

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

Uso:

import MapSyntax.MapOps

map1 plus map2

Unione di una sequenza di mappe:

maps.reduce(_ plus _)

0

Ho una piccola funzione per fare il lavoro, è nella mia piccola libreria per alcune funzionalità usate di frequente che non è nella lib standard. Dovrebbe funzionare con tutti i tipi di mappe, mutabili e immutabili, non solo con HashMaps

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

Ed ecco il corpo

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.