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

154
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
    @ Томаш: это то же самое ...
  • 0
    Самый простой это map1 ++ map2
Показать ещё 2 комментария
Теги:
merge
map

13 ответов

138
Лучший ответ

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 случай.)

  • 33
    Brilliant! Первый практический пример, в котором scalaz смысл.
  • 5
    Без шуток! Если вы начнете искать это ... это повсюду. Процитируем слова erric torrebone, автора спецификаций и спецификаций2: «Сначала вы изучаете Option и начинаете видеть его повсюду. Затем вы изучаете Applicative, и это то же самое. Далее?» Далее идут еще более функциональные концепции. И это очень помогает вам структурировать ваш код и хорошо решать проблемы.
Показать ещё 7 комментариев
140

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

map1 ++ map2.map{ case (k,v) => k -> (v + map1.getOrElse(k,0)) }
  • 31
    Хорошее решение. Мне нравится добавлять подсказку, что ++ заменяет любой (k, v) из карты на левой стороне ++ (здесь map1) на (k, v) с правой стороны карты, если (k, _) уже существует в левой части карты (здесь map1), например, Map(1->1) ++ Map(1->2) results in Map(1->2)
  • 0
    Вид аккуратной версии: for ((k, v) <- (aa ++ bb)) приводит к k -> (если ((aa содержит k) && (bb содержит k)) aa (k) + v, иначе v)
Показать ещё 10 комментариев
41

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

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

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

  • 0
    На данный момент это только неизменный Hashmap, но не изменяемый Hashmap.
  • 2
    Это довольно неприятно, что они имеют это только для HashMaps, чтобы быть честным.
Показать ещё 3 комментария
13

Это может быть реализовано как 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)
5

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

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

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

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

  import scalaz.Scalaz._
  map1 |+| map2
  • 11
    Вам нужно добавить немного больше подробностей в свой ответ, желательно код реализации. Сделайте это также для других похожих ответов, которые вы опубликовали, и подгоните каждый ответ к конкретному заданному вопросу. Полезное правило . Запрашивающий должен иметь возможность получить пользу от вашего ответа, не щелкая ссылку в блоге.
5
map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )
3

Вы также можете сделать это с помощью 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)
  • 0
    Eek, import cats.implicits._ . Импорт import cats.instances.map._ import cats.instances.int._ import cats.syntax.semigroup._ не намного более многословно ...
2

Ответ 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
1

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

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

Начиная 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)

0

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

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.

0

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню