Programing

두 개의 맵을 병합하고 동일한 키의 값을 합하는 가장 좋은 방법은 무엇입니까?

crosscheck 2020. 5. 30. 09:21
반응형

두 개의 맵을 병합하고 동일한 키의 값을 합하는 가장 좋은 방법은 무엇입니까?


val map1 = Map(1 -> 9 , 2 -> 20)
val map2 = Map(1 -> 100, 3 -> 300)

그것들을 병합하고 동일한 키의 값을 합산하고 싶습니다. 결과는 다음과 같습니다.

Map(2->20, 1->109, 3->300)

이제 두 가지 해결책이 있습니다.

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의 요청에 따라 조금 더 자세히.

수학적으로 세미 그룹 은 값 집합이며 해당 집합에서 두 개의 값을 가져와 해당 집합에서 다른 값을 생성하는 연산자와 함께 사용됩니다. 따라서 추가중인 정수는 세미 그룹입니다. 예를 들어 +연산자는 두 개의 정수를 결합하여 다른 정수를 만듭니다.

"주어진 키 유형 및 값 유형을 가진 모든 맵"세트에 대해 세미 그룹을 정의 할 수도 있습니다. 두 맵을 결합하여 새로운 맵을 생성하는 조작을 생성 할 수있는 한 입력.

두 맵에 모두 키가 없으면 사소한 것입니다. 동일한 키가 두 맵에 모두 존재하는 경우 키가 맵핑하는 두 값을 결합해야합니다. 흠, 우리는 같은 유형의 두 엔티티를 결합하는 연산자를 설명하지 않았습니까? 이것이 Scalaz에서 semigroup for Map[K, V]가 존재하는 경우에만 semigroup for 가 존재 하는 이유입니다.-semigroup for V- Vsemigroup은 동일한 키에 할당 된 두 맵의 값을 결합하는 데 사용됩니다.

따라서 Int여기에 값 유형이 있기 때문에 1의 "충돌" 은 두 개의 매핑 된 값을 정수로 추가하여 해결됩니다 (Int의 세미 그룹 연산자가하는 것과 같이) 100 + 9. 값이 문자열 인 경우 충돌로 인해 두 매핑 된 값의 문자열 연결이 발생했습니다 (다시 말하면 문자열에 대한 반 그룹 연산자가 수행하기 때문입니다).

(문자열 연결은 교환 법칙이 성립하지 않기 때문에 그리고 흥미롭게도, -,된다 "a" + "b" != "b" + "a"-. 반군 결과 작업은 그래서 어느 아닌 map1 |+| map2다른 map2 |+| map1문자열의 경우가 아니라 지능의 경우.)


내가 아는 가장 짧은 대답은 표준 라이브러리 만 사용한다는 것입니다.

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.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 로 구현할 수 있습니다 . 다음은 샘플 구현입니다. 이 방법을 사용하면 2 개가 아니라 맵 목록을 병합 할 수 있습니다.

// Monoid trait

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

The Map based implementation of the Monoid trait that merges two maps.

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
}

Now, if you have a list of maps that needs to be merged (in this case, only 2), it can be done like below.

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)

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

I wrote a blog post about this , check it out :

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

basically using scalaz semi group you can achieve this pretty easily

would look something like :

  import scalaz.Scalaz._
  map1 |+| map2

You can also do that with 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)

Starting Scala 2.13, another solution only based on the standard library consists in replacing the groupBy part of your solution with groupMapReduce which (as its name suggests) is an equivalent of a groupBy followed by mapValues and a reduce step:

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

This:

  • Concatenates the two maps as a sequence of tuples (List((1,9), (2,20), (1,100), (3,300))). For conciseness, map2 is implicitly converted to Seq to adapt to the type of map1.toSeq - but you could choose to make it explicit by using map2.toSeq,

  • groups elements based on their first tuple part (group part of groupMapReduce),

  • maps grouped values to their second tuple part (map part of groupMapReduce),

  • reduces mapped values (_+_) by summing them (reduce part of groupMapReduce).


Andrzej Doyle's answer contains a great explanation of semigroups which allows you to use the |+| operator to join two maps and sum the values for matching keys.

There are many ways something can be defined to be an instance of a typeclass, and unlike the OP you might not want to sum your keys specifically. Or, you might want to do operate on a union rather than an intersection. Scalaz also adds extra functions to Map for this purpose:

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

You can do

import scalaz.Scalaz._

map1 |+| map2 // As per other answers
map1.intersectWith(map2)(_ + _) // Do things other than sum the values

The fastest and simplest way:

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

By this way, each of element's immediately added to map.

The second ++ way is:

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

Unlike the first way, In a second way for each element in a second map a new List will be created and concatenated to the previous map.

The case expression implicitly creates a new List using unapply method.


Here's what I ended up using:

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

This is what I came up with...

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
}

I've got a small function to do the job, it's in my small library for some frequently used functionality which isn't in standard lib. It should work for all types of maps, mutable and immutable, not only HashMaps

Here is the usage

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

And here's the body

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

참고URL : https://stackoverflow.com/questions/7076128/best-way-to-merge-two-maps-and-sum-the-values-of-same-key

반응형