Лучший способ объединить две карты и суммировать значения одного и того же ключа?


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

Я хочу объединить их и суммировать значения одних и тех же ключей. Так что результат будет:

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

теперь у меня есть 2 решения:

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

и

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

но я хочу знать, есть ли какие-либо лучшие решения.

12 148

12 ответов:

Scalaz и понятие полугруппа который захватывает то, что вы хотите сделать здесь, и приводит к возможно самому короткому / самому чистому решению:

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)

в частности, бинарный оператор Map[K, V] комбинирует клавиши карт, складывая Vоператор полугруппы над любыми повторяющимися значениями. Стандартная полугруппа для Int использует оператор сложения, поэтому вы получаете сумму значений для каждого дубликата ключ.

Edit: немного больше деталей, согласно запросу user482745.

математически a полугруппа - это просто набор значений, вместе с оператором, который принимает два значения из этого набора, и производит другое значение из этого набора. Так что числа при сложении имеют полугруппа, например -+ оператор объединяет два ints, чтобы сделать еще один int.

вы также можете определить полугруппу по набору " все карты с a учитывая тип ключа и тип значения", пока вы можете придумать какую-то операцию, которая объединяет две карты, чтобы создать новую, которая каким-то образом является комбинацией двух входов.

если нет ключей, которые появляются в обеих картах, это тривиально. Если один и тот же ключ существует в обеих картах, то нам нужно объединить два значения, которые сопоставляет ключ. Хм, разве мы только что не описали оператор, который объединяет две сущности одного типа? Вот почему в Scalaz полугруппа для Map[K, V] существует тогда и только тогда, когда полугруппа для - используется для объединения значений из двух карт, которые назначены одному ключу.

потому что Int - Это тип значения здесь, в "столкновении" на 1 ключ решается целочисленным сложением двух отображенных значений (как это делает оператор полугруппы Int), следовательно 100 + 9. Если бы значения были строками, столкновение привело бы к конкатенации строк двух сопоставленных значений (опять же, потому что это то, что делает оператор полугруппы для строки).

(и интересно, потому что конкатенация строк не является коммутативным, то есть "a" + "b" != "b" + "a" - результирующая операция полугруппы также не является. Так что map1 |+| map2 отличается от map2 |+| map1 в случае строки, но не в случае Int.)

самый короткий ответ, который я знаю, что использует только стандартную библиотеку

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

быстрое решение:

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

Ну, теперь в библиотеке scala (по крайней мере, в 2.10) есть то, что вы хотели - слил

Это может быть реализовано как моноидом С просто скала. Вот пример реализации. При таком подходе мы можем объединить не просто 2, а целый список карт.

// Monoid trait

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

реализация на основе карты Моноидного признака, который объединяет две карты.

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
}

Теперь, если у вас есть список карт, которые необходимо объединить (в этом случае только 2), это можно сделать, как показано ниже.

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)
map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )

Я написал сообщение в блоге об этом, проверьте его:

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

в основном с помощью scalaz semi group вы можете достичь этого довольно легко

будет выглядеть так :

  import scalaz.Scalaz._
  map1 |+| map2

вы также можете сделать это с кошки.

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)

ответ Анджея Дойла содержит отличное объяснение полугрупп, которое позволяет использовать |+| оператор для объединения двух карт и суммирования значений для совпадающих ключей.

есть много способов что-то может быть определено как экземпляр класса typeclass, и в отличие от OP вы не можете суммировать свои ключи конкретно. Или, возможно, вы захотите работать на объединении, а не на пересечении. Scalaz также добавляет дополнительные функции в Map для этого цель:

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

можно сделать

import scalaz.Scalaz._

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

вот что я придумал...

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
}

у меня есть небольшая функция для выполнения этой работы, она находится в моей небольшой библиотеке для некоторых часто используемых функций, которые не входят в стандартную lib. Он должен работать для всех типов карт, изменяемых и неизменяемых, а не только HashMaps

вот использование

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

и вот тело

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

самый быстрый и простой способ:

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

таким образом, каждый из элементов сразу же добавляется на карту.

второй ++ путь:

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

В отличие от первого способа, во втором способе для каждого элемента во второй карте будет создан новый список и объединен с предыдущей картой.

The case выражение неявно создает новый список с помощью unapply метод.