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

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

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

Ответ 1

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

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 использует оператор сложения, поэтому вы получаете сумму значений для каждого повторяющегося ключа.

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

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

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

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

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

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

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

Ну, теперь в библиотеке scala (по крайней мере, в 2.10) есть что-то, что вы хотели - объединенная функция. НО он представлен только в HashMap не на карте. Это несколько сбивает с толку. Также подпись громоздка - не могу представить, почему мне нужен ключ дважды, и когда мне нужно будет создать пару с другим ключом. Но, тем не менее, он работает и намного чище, чем предыдущие "родные" решения.

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

Также в scaladoc упоминалось, что

Метод merged в среднем более эффективен, чем выполнение обход и реконструкция новой неизменной хэш-карты из нуля или ++.

Ответ 5

Это может быть реализовано как Monoid с помощью простого Scala. Вот пример реализации. При таком подходе мы можем объединить не только 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)

Ответ 6

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

Ответ 7

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

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

в основном с использованием полугруппы scalaz вы можете достичь этого довольно легко

будет выглядеть примерно так:

  import scalaz.Scalaz._
  map1 |+| map2

Ответ 8

Вы также можете сделать это с помощью 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)

Ответ 9

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

Существует много способов определить, каким образом может быть экземпляр класса 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

Ответ 10

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

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

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

Выражение case неявно создает новый список, используя метод unapply.

Ответ 11

Начиная Scala 2.13, другое решение только на основе стандартной библиотеки состоит в замене groupBy часть вашего раствора с K)(f:A=>B)(reduce:(B,B)=>B):scala.collection.immutable.Map[K,B] rel="nofollow noreferrer"> groupMapReduce, который (как предполагает его название) является эквивалентом groupBy с последующим mapValues и уменьшить шаг:

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

Это:

  • Объединяет две карты в виде последовательности кортежей (List((1,9), (2,20), (1,100), (3,300))). Для краткости map2 неявно преобразуется в Seq для адаптации к типу map1.toSeq - но вы можете сделать это явно с помощью map2.toSeq,

  • group элементы на основе их первой части кортежа (групповая часть группы MapReduce),

  • map сгруппированные значения с их второй частью кортежа (часть карты группы Map Reduce),

  • reduce сопоставленные значения (_+_), суммируя их (уменьшить часть groupMap Reduce).

Ответ 12

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

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
}

Ответ 13

У меня есть небольшая функция для выполнения этой работы, это в моей небольшой библиотеке для некоторых часто используемых функций, которые не входят в стандартную библиотеку. Он должен работать для всех типов карт, изменяемых и неизменных, не только 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

Ответ 14

Вот что я в итоге использовал:

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