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)) )
}
Но я хочу знать, есть ли лучшие решения.
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 случай.)
scalaz
смысл.
Самый короткий ответ, который я знаю об этом, использует только стандартную библиотеку
map1 ++ map2.map{ case (k,v) => k -> (v + map1.getOrElse(k,0)) }
++
заменяет любой (k, v) из карты на левой стороне ++
(здесь map1) на (k, v) с правой стороны карты, если (k, _) уже существует в левой части карты (здесь map1), например, Map(1->1) ++ Map(1->2) results in Map(1->2)
Быстрое решение:
(map1.keySet ++ map2.keySet).map {i=> (i,map1.getOrElse(i,0) + map2.getOrElse(i,0))}.toMap
Ну, теперь в библиотеке 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
в среднем более эффективен, чем выполнение обход и реконструкция новой неизменной хэш-карты из нуля или++
.
Это может быть реализовано как 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)
Я написал сообщение в блоге об этом, проверьте:
http://www.nimrodstech.com/scala-map-merge/
в основном с использованием полугруппы scalaz вы можете достичь этого довольно легко
будет выглядеть примерно так:
import scalaz.Scalaz._
map1 |+| map2
map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )
Вы также можете сделать это с помощью 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)
import cats.implicits._
. Импорт import cats.instances.map._ import cats.instances.int._ import cats.syntax.semigroup._
не намного более многословно ...
Ответ Andrzej Doyle содержит большое объяснение полугрупп, которое позволяет использовать оператор |+|
для объединения двух карт и суммирования значений для сопоставления ключей.
Существует много способов определить, каким образом может быть экземпляр класса 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
}
Начиная Scala 2.13
, другое решение только на основе стандартной библиотеки заключается в замене groupBy
части вашего решения с [groupMapReduce] (https://www.scala-lang.org/api/2.13.x/scala/collection/Seq. html # groupMapReduce K, B (f: A =% 3EB) (уменьшите: (B, B) =% 3EB): scala.collection.immutable.Map [K, B]), который (как следует из его названия) является эквивалентом groupBy
за которым следует mapValues
и шаг сокращения:
// val map1 = Map(1 -> 9, 2 -> 20)
// val map2 = Map(1 -> 100, 3 -> 300)
(map1.toSeq ++ map2.toSeq).groupMapReduce(_._1)(_._2)(_+_)
// Map[Int,Int] = Map(2 -> 20, 1 -> 109, 3 -> 300)
Это:
объединяет две карты в виде последовательности кортежей (List((1,9), (2,20), (1,100), (3,300))
)
group
элементы на основе их первой части кортежа (групповая часть группы MapReduce)
map
сгруппированные значения с их второй частью кортежа (часть карты группы Map Reduce)
reduce
значения (_+_
), суммируя их (уменьшить часть groupMap Reduce)
Самый быстрый и простой способ:
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
.
У меня есть небольшая функция для выполнения этой работы, это в моей небольшой библиотеке для некоторых часто используемых функций, которые не входят в стандартную библиотеку. Он должен работать для всех типов карт, изменяемых и неизменных, не только 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()
}
map1 ++ map2