Найкращий спосіб об'єднати дві карти і підсумувати значення одного ключа?


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

Але я хочу знати, чи є якісніші рішення.


Найпростішеmap1 ++ map2
Сераф

3
@Seraf Це насправді просто "зливає" карти, ігноруючи дублікати, а не підсумовуючи їх значення.
Зейнеп Аккаліонку Йілмаз

@ZeynepAkkalyoncuYilmaz правильно повинен був прочитати питання краще, залишаючи сором
Сераф

Відповіді:


143

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

Редагувати : трохи детальніше, відповідно до запиту користувача482745.

Математично напівгрупа - це лише набір значень разом з оператором, який приймає з цього набору два значення, і виробляє інше значення з цього набору. Отже цілі числа, що знаходяться під додаванням, - це напівгрупа, наприклад - +оператор поєднує два int, щоб зробити ще один int.

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

Якщо в обох картах немає клавіш, це тривіально. Якщо один і той же ключ існує в обох картах, то нам потрібно поєднати два значення, на які відображається ключ. Хм, хіба ми щойно не описали оператора, який поєднує два об'єкти одного типу? Ось чому в Scalaz напівгрупа for Map[K, V]існує, якщо і лише тоді, коли Semigroup for Vіснує - V, то напівгрупа використовується для об'єднання значень з двох карт, які присвоєні одному ключу.

Отже, оскільки тут Intє тип значення, "зіткнення" 1ключа вирішується цілим додаванням двох відображених значень (як це робить оператор напівгрупи Int), отже 100 + 9. Якби значення були Strings, зіткнення призвело б до об'єднання рядків двох відображених значень (знову ж таки, тому що це робить оператор напівгрупи для String).

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


37
Блискуче! Перший практичний приклад, де scalazмав сенс.
соц

5
Без жартів! Якщо ви почнете його шукати ... це все повсюдно. Цитуючи ерічного торребона автора специфікацій та specs2: "Спочатку ви дізнаєтесь Варіант, і ви починаєте його бачити всюди. Потім ви вивчаєте додаток, і це те саме. Далі?" Далі - ще більш функціональні поняття. І вони дуже допомагають вам структурувати код та вирішувати проблеми.
AndreasScheinert

4
Власне, я шукав Варіант п’ять років, коли нарешті знайшов Скалу. Різниця між посиланням на об’єкт Java, яка може бути нульовою, і такою, яка не може бути (тобто між Aі Option[A]), настільки величезна, що я не міг повірити, що вони справді одного типу. Я тільки почав дивитися на Скалаза. Я не впевнений, що я досить розумний ...
Мальволіо

1
Існує також варіант для Java, див. Функціональна Java. Не майте ніякого страху, навчання - це весело. А функціональне програмування не навчає вас лише нових речей, але натомість пропонує програмісту допомогу з наданням термінів, словника для вирішення проблем. Питання про ОП - прекрасний приклад. Концепція напівгрупи настільки проста, ви її використовуєте щодня, наприклад, для Strings. Справжня сила з'являється, якщо ви визначите цю абстракцію, назвіть її і, нарешті, застосуйте її до інших типів, а не просто String.
AndreasScheinert

1
Як можливо, що це призведе до 1 -> (100 + 9)? Ви можете, будь ласка, показати мені «стек стежка»? Дякую. PS: Я прошу тут зробити більш чітку відповідь.
user482745

152

Найкоротша відповідь, яку я знаю, що використовує лише стандартну бібліотеку

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

34
Приємне рішення. Мені подобається додати підказку, яка ++замінює будь-яку (k, v) з карти з лівого боку ++(тут map1) на (k, v) з карти правого боку, якщо (k, _) вже існує зліва бічна карта (тут map1), наприкладMap(1->1) ++ Map(1->2) results in Map(1->2)
Lutz

Різновидна витончена версія: для ((k, v) <- (aa ++ bb)) вихід k -> (якщо ((aa містить k) && (bb містить k)) aa (k) + v else v)
ділбізеро

Я раніше щось робив, але тут є версія того, що ви зробили, замінивши карту на formap1 ++ (для ((k, v) <- map2) вихід k -> (v + map1.getOrElse (k, 0 )))
ділбізеро

1
@ Jus12 - Ні. .Має більшу перевагу ніж ++; ви читаєте map1 ++ map2.map{...}як map1 ++ (map2 map {...}). Отже, один спосіб відображає map1елементи s, а інший - немає.
Рекс Керр

1
@matt - Scalaz вже зробить це, тому я б сказав, "існуюча бібліотека вже це робить".
Рекс Керр


41

Ну, а тепер у бібліотеці scala (принаймні в 2.10) є щось, що ви хотіли - об'єднана функція. Але НЕ представлено лише в HashMap, а не в Map. Це дещо заплутано. Також підпис громіздкий - не уявляю, для чого мені потрібен ключ двічі і коли мені потрібно створити пару з іншим ключем. Але, тим не менш, він працює і набагато чистіше, ніж попередні "рідні" рішення.

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

Також у скаладоку згадували про це

mergedМетод в середньому продуктивний більше , ніж робить обхід і відновлення нового незмінного хеш - карту з нуля, або ++.


1
На даний момент це лише в незмінному Hashmap, а не в змінному Hashmap.
Кевін Вілер

2
Це дуже дратує, що вони мають лише те, що чесні HashMaps.
Йохан S

Я не можу змусити це компілювати, схоже, тип, який він приймає, є приватним, тому я не можу передати введену функцію, яка відповідає.
Райан Ліч

2
Здається, щось змінилось у версії 2.11. Перевірте 2.10 scaladoc - scala-lang.org/api/2.10.1/… Є звичайна функція. Але в 2.11 це MergeFunction.
Михайло Голубцов

Все, що змінилося в 2.11, - це введення псевдоніма типу для цього конкретного типу функціїprivate type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1)
EthanP

14

Це може бути реалізовано як моноїд із просто простою Скалою. Ось приклад реалізації. При такому підході ми можемо об'єднати не просто 2, а список карт.

// Monoid trait

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

На Картах реалізація ознаки Monoid, яка об'єднує дві карти.

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

Ви також можете це зробити з 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._не набагато багатослівніше ...
St.Antario

@ St.Antario це насправді рекомендований спосіб мати тількиimport cats.implicits._
Арцьомій Міклушоу

Рекомендовано ким? Внесення всіх (неякісно невикористаних) неявних екземплярів у сферу ускладнює життя компілятора. І крім того, якщо одному не потрібен, скажімо, прикладний примірник, навіщо вони його туди привозять?
St.Antario

4

По-перше Scala 2.13, ще одне рішення, що базується лише на стандартній бібліотеці, полягає в заміні тієї groupByчастини вашого рішення, 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,

  • groups елементів, заснованих на їх першій кортежній частині (групова частина групи MapReduce),

  • maps згруповані значення до їх другої кортежної частини (карта частини групи зменшення карти ),

  • reduceз перетвореними значеннями ( _+_) шляхом підсумовування їх (зменшити частину groupMap Зменшити ).


3

Ось що я в кінцевому підсумку використав:

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

1
Це насправді не суттєво відрізняється від першого рішення, запропонованого ОП.
jwvh

2

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

Є багато способів, як щось можна визначити як екземпляр класу типу, і на відміну від ОП, можливо, ви не хочете конкретно підсумовувати свої ключі. Або, можливо, ви хочете діяти на союзі, а не на перехресті. Для цього 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

2

Найшвидший і найпростіший спосіб:

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метод.


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
}

1

За допомогою шаблону typeclass ми можемо об'єднати будь-який числовий тип:

object MapSyntax {
  implicit class MapOps[A, B](a: Map[A, B]) {
    def plus(b: Map[A, B])(implicit num: Numeric[B]): Map[A, B] = {
      b ++ a.map { case (key, value) => key -> num.plus(value, b.getOrElse(key, num.zero)) }
    }
  }
}

Використання:

import MapSyntax.MapOps

map1 plus map2

Об'єднання послідовності карт:

maps.reduce(_ plus _)

0

У мене є невелика функція, щоб виконувати цю роботу, це в моїй невеликій бібліотеці для часто використовуваних функціональних можливостей, які не є в стандартній 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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.