Лучший способ объединить две карты и суммировать значения одного и того же ключа?
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 ответов:
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
Это может быть реализовано как моноидом С просто скала. Вот пример реализации. При таком подходе мы можем объединить не просто 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)
Я написал сообщение в блоге об этом, проверьте его:
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
для этого цель:можно сделать
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() }
самый быстрый и простой способ:
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
метод.