Melhor maneira de mesclar dois mapas e somar os valores da mesma chave?


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

Quero mesclá-los e somar os valores das mesmas chaves. Então o resultado será:

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

Agora eu tenho 2 soluções:

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

Mas quero saber se existem soluções melhores.


O mais fácil émap1 ++ map2
Seraf

3
@Seraf Isso simplesmente "mescla" os mapas, ignorando duplicatas em vez de somar seus valores.
Zeynep Akkalyoncu Yilmaz 29/09

@ZeynepAkkalyoncuYilmaz direito deve ter lido a pergunta melhor, folhas de vergonha
Seraf

Respostas:


143

O Scalaz tem o conceito de um semigrupo que captura o que você quer fazer aqui e leva à possível solução mais curta / limpa:

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)

Especificamente, o operador binário para Map[K, V]combina as chaves dos mapas, dobrando Vo operador do semigrupo sobre quaisquer valores duplicados. O semigrupo padrão para Intusa o operador de adição, para que você obtenha a soma dos valores para cada chave duplicada.

Editar : um pouco mais detalhadamente, conforme a solicitação do usuário482745.

Matematicamente, um semigrupo é apenas um conjunto de valores, junto com um operador que pega dois valores desse conjunto e produz outro valor a partir desse conjunto. Portanto, números inteiros em adição são um semigrupo, por exemplo - o +operador combina duas entradas para criar outra int.

Você também pode definir um semigrupo sobre o conjunto de "todos os mapas com um determinado tipo de chave e tipo de valor", desde que seja possível criar alguma operação que combine dois mapas para produzir um novo, que seja de alguma forma a combinação dos dois entradas.

Se não houver teclas que apareçam nos dois mapas, isso é trivial. Se a mesma chave existir nos dois mapas, precisamos combinar os dois valores para os quais a chave é mapeada. Hmm, não acabamos de descrever um operador que combina duas entidades do mesmo tipo? É por isso que no Scalaz Map[K, V]existe um semigrupo para se, e somente se, um semigrupo para Vexistir - Vé usado para combinar os valores de dois mapas atribuídos à mesma chave.

Portanto, como Inté o tipo de valor aqui, a "colisão" na 1chave é resolvida pela adição inteira dos dois valores mapeados (como é o que o operador de semigrupo do Int faz), portanto 100 + 9. Se os valores tivessem sido Strings, uma colisão resultaria na concatenação de string dos dois valores mapeados (novamente, porque é isso que o operador de semigrupo para String faz).

(E, curiosamente, porque a concatenação de strings não é comutativa - ou seja, "a" + "b" != "b" + "a"- a operação de semigrupo resultante também não é. Portanto, map1 |+| map2é diferente do map2 |+| map1caso String, mas não no caso Int).


37
Brilhante! Primeiro exemplo prático onde scalazfazia sentido.
soc

5
Sem brincadeiras! Se você começar a procurar ... está em todo lugar. Para citar erric torrebone, autor de especificações e especificações2: "Primeiro você aprende Opção e começa a vê-la em todos os lugares. Depois, aprende Aplicável e é a mesma coisa. Próximo?" A seguir, conceitos ainda mais funcionais. E isso ajuda muito a estruturar seu código e a resolver problemas de maneira adequada.
AndreasScheinert

4
Na verdade, eu procurava a Option há cinco anos quando finalmente encontrei Scala. A diferença entre uma referência de objeto Java que pode ser nula e uma que não pode ser (ou seja, entre Ae Option[A]) é tão grande que eu não podia acreditar que eles eram realmente do mesmo tipo. Eu apenas comecei a olhar para Scalaz. Não tenho certeza de que sou inteligente o suficiente ...
Malvolio

1
Também existe a opção para Java, consulte Java funcional. Não tenha medo, aprender é divertido. E a programação funcional não ensina coisas novas (apenas), mas oferece a ajuda do programador no fornecimento de termos e vocabulário para resolver problemas. A questão do OP é um exemplo perfeito. O conceito de um semigrupo é tão simples que você o usa todos os dias, por exemplo, por strings. O poder real aparece se você identificar essa abstração, nomeá-la e finalmente aplicá-la a outros tipos, em seguida, apenas a String.
precisa saber é o seguinte

1
Como é possível que isso resulte em 1 -> (100 + 9)? Você pode me mostrar "rastreamento de pilha"? THX. PS: Estou pedindo aqui para tornar a resposta mais clara.
user482745

152

A resposta mais curta que conheço que usa apenas a biblioteca padrão é

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

34
Ótima solução. Eu gosto de adicionar a dica, que ++substitui qualquer (k, v) do mapa no lado esquerdo de ++(aqui map1) por (k, v) do mapa do lado direito, se (k, _) já existir no lado esquerdo mapa lateral (aqui map1), por exemploMap(1->1) ++ Map(1->2) results in Map(1->2)
Lutz

Um tipo de versão mais limpa: para ((k, v) <- (aa ++ bb)) produz k -> (se ((aa contém k) && (bb contém k)) aa (k) + v else v)
dividebyzero

Eu fiz algo diferente anteriormente, mas aqui está uma versão do que você fez, substituindo o mapa por um formap1 ++ (para ((k, v) <- map2) produz k -> (v + map1.getOrElse (k, 0 )))
dividebyzero

1
@ Jus12 - No. .tem precedência mais alta que ++; você lê map1 ++ map2.map{...}como map1 ++ (map2 map {...}). Então, de um jeito que você mapeia os map1elementos de s, e do outro, não.
Rex Kerr

1
@matt - O Scalaz já fará isso, então eu diria "uma biblioteca existente já faz".
Rex Kerr #

48

Solução rápida:

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

41

Bem, agora na biblioteca scala (pelo menos na 2.10) há algo que você queria - função mesclada . MAS é apresentado apenas no HashMap e não no Mapa. É um pouco confuso. Além disso, a assinatura é complicada - não consigo imaginar por que eu precisaria de uma chave duas vezes e quando precisaria produzir um par com outra chave. Porém, ele funciona e é muito mais limpo que as soluções "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) })

Também no scaladoc mencionou que

O mergedmétodo é, em média, mais eficiente do que fazer uma travessia e reconstruir um novo mapa de hash imutável do zero, ou ++.


1
A partir de agora, ele está apenas no Hashmap imutável, não no Hashmap mutável.
Kevin Wheeler

2
É muito chato que eles só tenham isso para que os HashMaps sejam honestos.
Johan S

Não consigo compilar isso, parece que o tipo que ele aceita é privado; portanto, não posso transmitir uma função digitada que corresponda.
Ryan The Leach

2
Parece que algo mudou na versão 2.11. Confira 2.10 scaladoc - scala-lang.org/api/2.10.1/… Existe uma função usual. Mas na versão 2.11 é MergeFunction.
Mikhail Golubtsov

Tudo o que mudou no 2.11 é a introdução de um alias de tipo para esse tipo de função em particularprivate type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1)
EthanP

14

Isso pode ser implementado como um Monoid com Scala simples. Aqui está uma implementação de amostra. Com essa abordagem, podemos mesclar não apenas 2, mas uma lista de mapas.

// Monoid trait

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

A implementação baseada no mapa da característica Monoid que mescla dois 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
}

Agora, se você tiver uma lista de mapas que precisam ser mesclados (neste caso, apenas 2), isso pode ser feito como abaixo.

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

Eu escrevi um post sobre isso, confira:

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

basicamente usando semi grupo scalaz, você pode conseguir isso facilmente

seria algo como:

  import scalaz.Scalaz._
  map1 |+| map2

11
Você precisa colocar um pouco mais de detalhes em sua resposta, de preferência algum código de implementação. Faça isso também para as outras respostas semelhantes que você postou e adapte cada resposta à pergunta específica que foi feita. Regra geral: o solicitante deve se beneficiar da sua resposta sem clicar no link do blog.
Robert Harvey

5

Você também pode fazer isso com 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)

Eek import cats.implicits._. Importar import cats.instances.map._ import cats.instances.int._ import cats.syntax.semigroup._não muito mais detalhado ...
St.Antario

@ St.Antario é realmente recomendado caminho só para terimport cats.implicits._
Artsiom Miklushou

Recomendado por quem? Trazer todas as instâncias implícitas (a maioria não utilizadas) para o escopo complica a vida do compilador. E, além disso, se não for necessário, digamos, instância aplicável, por que eles a levariam para lá?
Paulo #

4

Iniciando Scala 2.13, outra solução baseada apenas na biblioteca padrão consiste em substituir a groupByparte da sua solução pela groupMapReducequal (como o nome sugere) é equivalente a uma etapa groupByseguida por mapValuese uma etapa de redução:

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

Este:

  • Concatena os dois mapas como uma sequência de tuplas ( List((1,9), (2,20), (1,100), (3,300))). Por motivos de concisão, map2é implicitamente convertido em Seqpara se adaptar ao tipo de map1.toSeq- mas você pode optar por torná-lo explícito usando map2.toSeq,

  • groups elementos baseados na primeira parte da tupla (parte do grupo MapReduce),

  • maps agruparam valores na segunda parte da tupla (mapear parte do grupo Map Reduce),

  • reduces valores mapeados ( _+_) somando-os (reduza parte do groupMap Reduce ).


3

Aqui está o que eu acabei usando:

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

1
Na verdade, isso não é significativamente diferente da primeira solução proposta pelo OP.
jwvh

2

A resposta de Andrzej Doyle contém uma ótima explicação dos semigrupos, que permite usar o |+|operador para unir dois mapas e somar os valores das chaves correspondentes.

Há muitas maneiras pelas quais algo pode ser definido para ser uma instância de uma classe de tipo e, diferentemente do OP, você pode não querer somar suas chaves especificamente. Ou, talvez você queira operar em uma união e não em um cruzamento. O Scalaz também adiciona funções extras Mappara este fim:

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

Você pode fazer

import scalaz.Scalaz._

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

2

A maneira mais rápida e simples:

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

Dessa forma, cada elemento é adicionado imediatamente ao mapa.

A segunda ++maneira é:

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

Diferentemente da primeira maneira, em uma segunda maneira para cada elemento em um segundo mapa, uma nova Lista será criada e concatenada ao mapa anterior.

A caseexpressão cria implicitamente uma nova lista usando o unapplymétodo


1

Isto é o que eu vim com ...

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 o padrão tipeclass, podemos mesclar qualquer tipo numérico:

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

Mesclando uma sequência de mapas:

maps.reduce(_ plus _)

0

Eu tenho uma função pequena para fazer o trabalho, é na minha pequena biblioteca algumas funcionalidades frequentemente usadas que não estão na biblioteca padrão. Deve funcionar para todos os tipos de mapas, mutáveis ​​e imutáveis, não apenas HashMaps

Aqui está o 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

E aqui está o 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%2Fextensions%2Fpackage.scala#L190

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.