web-dev-qa-db-ja.com

2つのマップをマージして同じキーの値を合計する最良の方法は?

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

しかし、もっと良い解決策があるかどうか知りたいです。

163
Freewind

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の標準セミグループは加算演算子を使用するため、各重複キーの値の合計を取得します。

Edit:user482745のリクエストによると、もう少し詳細です。

数学的には、 semigroup は値のセットであり、そのセットから2つの値を取り、そのセットから別の値を生成する演算子があります。そのため、たとえば、加算中の整数はセミグループです-+演算子は2つのintを組み合わせて別のintを作成します。

また、2つのマップを組み合わせて何らかの方法で2つのマップを組み合わせた新しいマップを作成する操作を考え出すことができる限り、「特定のキータイプと値タイプを持つすべてのマップ」のセットでセミグループを定義することもできます入力。

両方のマップに表示されるキーがない場合、これは簡単です。同じキーが両方のマップに存在する場合、キーがマップする2つの値を結合する必要があります。うーん、同じ種類の2つのエンティティを結合する演算子を説明しただけではありませんか?これが、ScalazでMap[K, V]のセミグループが存在するのは、Vのセミグループが存在する場合にのみ存在する理由です-Vのセミグループは、同じキーに割り当てられた2つのマップの値を結合するために使用されます。

したがって、Intはここでは値型であるため、1キーの「衝突」は、2つのマップされた値の整数加算によって解決されます(Intのセミグループ演算子が行うように)。したがって、100 + 9。値が文字列であった場合、衝突により、2つのマップされた値の文字列連結が発生します(これも、文字列のセミグループ演算子が行うためです)。

(そして興味深いことに、文字列の連結は可換ではないので、つまり"a" + "b" != "b" + "a"-セミグループ演算の結果も同じではありません。したがって、文字列の場合はmap1 |+| map2map2 |+| map1と異なりますが、 Intの場合。)

140
Andrzej Doyle

私が知っている最短の答えは、標準ライブラリのみを使用することです

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

クイックソリューション:

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

さて、今ではscalaライブラリ(少なくとも2.10では)に何か欲しいものがあります-merged function。ただし、マップではなくHashMapでのみ表示されます。少しわかりにくいです。また、署名は面倒です-なぜキーを2回必要とするのか、いつ別のキーとペアを作成する必要があるのか​​想像できません。しかし、それでも、以前の「ネイティブ」ソリューションよりも機能し、はるかにクリーンです。

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メソッドは、トラバーサルを行って新しい不変ハッシュマップを最初から再構築するか、++よりも平均してパフォーマンスが高くなります。

38

これは、単なるScalaで Monoid として実装できます。以下に実装例を示します。このアプローチを使用すると、2つだけでなく、マップのリストをマージできます。

// Monoid trait

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

2つのマップをマージする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)
13
Jegan

これについてのブログ記事を書いたので、チェックしてみてください。

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

基本的にscalazセミグループを使用すると、これを簡単に実現できます

次のようになります。

  import scalaz.Scalaz._
  map1 |+| map2
5
Nimrod007

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)
5
map1 ++ ( for ( (k,v) <- map2 ) yield ( k -> ( v + map1.getOrElse(k,0) ) ) )
5
AmigoNico

Andrzej Doyleの答えには、セミグループの優れた説明が含まれており、|+|演算子を使用して2つのマップを結合し、一致するキーの値を合計できます。

タイプクラスのインスタンスとして何かを定義できる方法はたくさんあります。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
user1158559

最速かつ最も簡単な方法:

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

この方法により、各要素はすぐにマップに追加されます。

2番目の++の方法は次のとおりです。

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

最初の方法とは異なり、2番目の方法では、2番目のマップの各要素に対して新しいリストが作成され、前のマップに連結されます。

case式は、unapplyメソッドを使用して新しいリストを暗黙的に作成します。

2

私が最終的に使用したものは次のとおりです。

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

これは私が思いついたものです...

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
kaur

私は仕事をする小さな関数を持っています、それは標準ライブラリにないいくつかの頻繁に使用される機能のための私の小さなライブラリにあります。 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#L19

0
Eugene Platonov