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


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

그러나 더 나은 솔루션이 있는지 알고 싶습니다.


가장 쉬운 방법map1 ++ map2
Seraf

3
@Seraf 실제로는 단순히 값을 합산하는 대신 중복을 무시하고 맵을 "병합"합니다.
Zeynep Akkalyoncu Yilmaz

@ZeynepAkkalyoncuYilmaz 오른쪽 질문을 더 잘 읽고 부끄러워 나뭇잎
Seraf

답변:


143

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문자열의 경우가 아니라 지능의 경우.)


37
훌륭한! scalaz이해 되는 첫 번째 실제 예 .
soc

5
농담 아니야! 당신이 그것을 찾기 시작하면 ... 그것은 도처에 있습니다. erric torrebone의 스펙과 스펙의 저자 2를 인용하려면 : "먼저 Option을 배우고 어디서나 볼 수 있습니다. 그런 다음 Applicative를 배우고 같은 것입니다. 다음?" 다음은 훨씬 더 기능적인 개념입니다. 또한 코드를 구성하고 문제를 멋지게 해결하는 데 큰 도움이됩니다.
AndreasScheinert

4
사실, 나는 스칼라를 마침내 찾았을 때 5 년 동안 옵션을 찾고있었습니다. null 수 있는 Java 객체 참조 와 ( A및 사이 에 Option[A]) 불가능 할 수 있는 Java 객체 참조의 차이 가 너무 커서 실제로 동일한 유형이라고 믿을 수 없었습니다. 나는 단지 Scalaz보고 시작했다. 나는 내가 충분히 똑똑하지 않다 ...
Malvolio

1
Java 옵션도 있습니다. 기능적 Java를 참조하십시오. 학습이 재미 있다는 두려움이 없습니다. 함수형 프로그래밍은 새로운 것을 가르쳐주지 않고 (단지) 문제를 해결하기 위해 용어, 어휘를 제공하는 데 도움을주는 프로그래머의 도움을 제공합니다. OP 질문은 완벽한 예입니다. 세미 그룹의 개념은 매우 간단합니다. 예를 들어 문자열과 같이 매일 사용합니다. 이 추상화를 식별하고 이름을 지정한 다음 마지막으로 다른 유형에 적용하면 String이됩니다.
AndreasScheinert

1
어떻게 1-> (100 + 9)가 될 수 있습니까? "스택 추적"을 보여 주시겠습니까? 고마워. 추신 : 나는 여기에 더 명확한 대답을 요구하고 있습니다.
482745

152

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

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

34
좋은 해결책. 힌트를 추가하고 싶습니다. (k, _)가 이미 왼쪽에있는 경우 (여기서 map1) ++왼쪽의 맵에서 ++(k, v)를 오른쪽 맵의 (k, v)로 바꿉니다. 사이드 맵 (여기에서 map1), 예 :Map(1->1) ++ Map(1->2) results in Map(1->2)
Lutz

더 깔끔한 버전 : for ((k, v) <-(aa ++ bb)) yield k-> (((aa는 k를 포함하고 && (bb는 k를 포함)) aa (k) + v else v)
splitbyzero

이전에 다른 방식으로 수행했지만 formap1 ++ (((k, v) <-map2) yield k-> (v + map1.getOrElse (k, 0) 의 맵을 대체하는 작업의 버전이 있습니다. )))
splitbyzero

1
@ Jus12-No . .보다 우선 순위가 높습니다 ++. 당신은 map1 ++ map2.map{...}로 읽습니다 map1 ++ (map2 map {...}). 한 가지 방법은 map1요소 를 매핑 하고 다른 방법은 그렇지 않은 방법입니다.
Rex Kerr

1
@ matt-Scalaz는 이미 그렇게 할 것이므로 "기존 라이브러리가 이미 수행하고 있습니다"라고 말하고 싶습니다.
Rex Kerr

48

빠른 솔루션 :

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

41

자, 스칼라 라이브러리 (적어도 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방법은 순회를 수행하고 새로운 불변 ​​해시 맵을 처음부터 재구성하는 것보다 평균적으로 성능이 뛰어납니다 ++.


1
현재로서는 변경할 수없는 해시 맵이 아닌 변경할 수없는 해시 맵에만 있습니다.
Kevin Wheeler

2
이것은 HashMaps가 정직해야한다는 것을 매우 성가시다.
Johan S

나는 이것을 컴파일 할 수 없다, 그것이 받아들이는 타입은 private 인 것 같아서, 일치하는 타입이 지정된 함수를 전달할 수 없다.
라이언 리치

2
2.11 버전에서 변경된 사항이있는 것 같습니다. 2.10 scaladoc 체크 아웃 - scala-lang.org/api/2.10.1/... 일반적인 기능이 있습니다. 그러나 2.11에서는 MergeFunction입니다.
Mikhail Golubtsov

2.11에서 변경된 것은이 특정 함수 유형에 대한 유형 별명을 도입하는 것입니다.private type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1)
EthanP

14

평범한 스칼라만으로도 Monoid 로 구현할 수 있습니다 . 다음은 샘플 구현입니다. 이 방법을 사용하면 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 semi 그룹을 사용하면이를 쉽게 달성 할 수 있습니다

다음과 같이 보일 것입니다 :

  import scalaz.Scalaz._
  map1 |+| map2

11
대답에 좀 더 자세하게 설명해야하며, 구현 코드가 바람직합니다. 게시 한 다른 유사한 답변에 대해서도이 작업을 수행하고 요청 된 특정 질문에 대한 각 답변을 조정하십시오. 경험의 규칙 : asker는 블로그 링크를 클릭하지 않고도 답변을 얻을 수 있습니다.
Robert Harvey

5

당신은 또한 고양이 와 함께 할 수 있습니다 .

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)

EEK, import cats.implicits._. import cats.instances.map._ import cats.instances.int._ import cats.syntax.semigroup._훨씬 더 장황한 가져 오기 ...
St.Antario

@ St.Antario 그것은 실제로 가지고있는 방법을 권장합니다import cats.implicits._
Artsiom Miklushou

누구에게 추천? 암시 적 인스턴스를 사용하지 않는 대부분의 인스턴스를 범위로 가져 오면 컴파일러의 수명이 복잡해집니다. 그리고 필요하지 않다면, 예를 들어, 적용 사례는 왜 그것을 가져올까요?
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,

  • group첫 튜플 부 (군의 부분에 기초하여 요소 S는 그룹 의 MapReduce)

  • map두 번째 튜플 부분 (그룹 Map Reduce 의 맵 부분)에 그룹화 된 값

  • reduces 값 _+_을 합산하여 맵핑 된 값 ( ) (groupMap Reduce의 일부를 줄임 ).


3

내가 사용한 결과는 다음과 같습니다.

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

1
이는 OP가 제안한 첫 번째 솔루션과 크게 다르지 않습니다.
jwvh

2

Andrzej Doyle의 답변에는 |+|연산자를 사용하여 두 개의 맵을 조인하고 일치하는 키의 값을 합할 수있는 세미 그룹에 대한 훌륭한 설명이 포함되어 있습니다 .

유형 클래스의 인스턴스로 정의 할 수있는 방법은 여러 가지가 있으며 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

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메서드를 사용하여 암시 적으로 새 List를 만듭니다 .


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

작업을 수행하는 작은 기능이 있으며 표준 라이브러리에없는 자주 사용되는 기능을 위해 작은 라이브러리에 있습니다. 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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.