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


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
Seraf

3
@Serafこれは実際には単純にマップを「マージ」し、値を合計する代わりに重複を無視します。
Zeynep Akkalyoncu Yilmaz

@ZeynepAkkalyoncuYilmaz質問をよく読んだ方がいい、恥ずかしいままに
Seraf

回答:


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は加算演算子を使用するため、重複する各キーの値の合計を取得できます。

編集:user482745のリクエストに従って、もう少し詳細。

数学的には、セミグループは単なる値のセットであり、そのセットから2つの値を取り、そのセットから別の値を生成する演算子を備えています。たとえば、加算中の整数はセミグループです。たとえば、+演算子は2つのintを結合して別のintを作成します。

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

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

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

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


37
鮮やかさ!scalaz理にかなった最初の実用的な例。
soc

5
冗談じゃない!あなたがそれを探し始めたら...それは至る所にあります。スペックとスペックの作者であるtorreboneの著者を引用するには:「最初にOptionを学び、どこからでもそれを見始めます。それからApplicativeを学び、それは同じことです。次は?」次はさらに機能的な概念です。これらは、コードを構造化して問題を適切に解決するのに非常に役立ちます。
AndreasScheinert、2011

4
実は、ようやくScalaを見つけた5年間、オプションを探していました。nullの可能性があるJavaオブジェクト参照とそうでない可能性のあるJavaオブジェクト参照(つまり、Aとの間Option[A])の違いは非常に大きいため、実際には同じ型であるとは信じられませんでした。私はスカラズを見始めたばかりです。私は十分賢いのか
わかり

1
Option for Javaもあります。FunctionalJavaを参照してください。恐れる必要はありません、学習は楽しいです。また、関数型プログラミングは新しいことだけを教えるわけではありませんが、代わりに、プログラマーが問題に取り組むための用語や語彙を提供するのに役立ちます。OPの質問は完璧な例です。セミグループのコンセプトはとてもシンプルなので、例えばストリングスのように毎日使用します。この抽象化を識別して名前を付け、最後にそれを他の型に適用して、文字列だけにすると、真の力が発揮されます。
AndreasScheinert、2011

1
1->(100 + 9)になる可能性はありますか?「スタックトレース」を見せていただけますか?どうも。PS:私はここで答えをより明確にするように求めています。
user482745

152

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

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

34
素晴らしい解決策。(ここでmap1)の++左側にあるマップの++(k、v)を右側のマップの(k、v)で置き換えます(左側に(k、_)が既に存在する場合)。サイドマップ(ここではmap1)、例Map(1->1) ++ Map(1->2) results in Map(1->2)
Lutz

より良いバージョンの一種:for((k、v)<-(aa ++ bb))はk->(if((aa contains k)&&(bb contains k))aa(k)+ v else v)
dividebyzero 2014

以前は別の方法で行いましたが、これはあなたがしたことのバージョンで、formap1 ++のマップを置き換えます(for((k、v)<-map2)yield k->(v + map1.getOrElse(k、0 )))
dividebyzero 2014

1
@ Jus12-いいえ 。.優先順位はよりも高くなり++ます。あなたはと読みmap1 ++ map2.map{...}ますmap1 ++ (map2 map {...})。つまり、1つの方法でmap1s要素をマッピングし、もう1つの方法ではマッピングしません。
Rex Kerr

1
@matt-Scalazはすでにそれを実行するので、「既存のライブラリーはすでにそれを実行します」と言います。
レックスカー


41

さて、スカラライブラリ(少なくとも2.10では)には、あなたが望むものがあります。ただし、マップではなく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) })

また、スカラドックでは、

このmerged方法は、トラバーサルを実行して、新しい不変のハッシュマップを最初から再構築するよりも、平均してパフォーマンスが優れています++


1
現在のところ、変更可能なハッシュマップではなく、不変のハッシュマップにのみ存在します。
Kevin Wheeler

2
これは、HashMapsが正直であるためだけにあるので、非常に不愉快です。
ヨハンS

これをコンパイルすることはできません、それが受け入れる型はプライベートのようですので、一致する型付き関数を渡すことができません。
ライアンザリーチ2015

2
2.11バージョンで何かが変更されたようです。2.10 scaladocをチェックアウト- scala-lang.org/api/2.10.1/...通常の機能があります。しかし、2.11ではMergeFunctionです。
ミハイルゴルブツォフ2015

2.11で変更されたのは、この特定の関数タイプのタイプエイリアスの導入ですprivate type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1)
EthanP

14

これは単純なScalaでモノイドとして実装できます。以下はサンプル実装です。このアプローチでは、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)


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)

Eek 、import cats.implicits._. import cats.instances.map._ import cats.instances.int._ import cats.syntax.semigroup._それほど詳細ではないインポート...
St.Antario '14

@ St.Antarioそれだけが実際に推奨される方法import cats.implicits._
Artsiom Miklushou

誰が推薦しましたか?すべて(ほとんどが未使用)の暗黙のインスタンスをスコープに入れると、コンパイラーの寿命が複雑になります。そして、もし必要としないなら、例えば、Applicativeインスタンスがなぜそれをそこに持ってくるのでしょうか?
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)

この:

  • 2つのマップをタプルのシーケンスとして連結します(List((1,9), (2,20), (1,100), (3,300)))。簡潔にするために、map2された暗黙的に変換Seqのタイプに適応するmap1.toSeq-しかし、あなたが使用して、それを明示的にするために選択することができmap2.toSeq

  • groups最初のタプル部分(グループ MapReduceのグループ部分)に基づく要素

  • map■グループ化された値を2番目のタプル部分(グループMap Reduceのマップ部分)に

  • reducesマップされた値(_+_)を合計して(groupMap Reduceの一部を削減)。


3

これが私が最終的に使用したものです:

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

1
これは、OPによって提案された最初のソリューションとそれほど変わりません。
jwvh

2

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

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

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する方法を。


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.