วิธีที่ดีที่สุดในการผสานสองแผนที่และรวมค่าของคีย์เดียวกัน


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มีแนวคิดของ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ตัวดำเนินการ semigroup ของการพับทับค่าที่ซ้ำกันใด ๆ semigroup มาตรฐานสำหรับIntใช้ตัวดำเนินการเพิ่มดังนั้นคุณจะได้รับผลรวมของค่าสำหรับแต่ละคีย์ที่ซ้ำกัน

แก้ไข : รายละเอียดเพิ่มเติมเล็กน้อยตามคำขอของผู้ใช้ 482745

ศาสตร์semigroupเป็นเพียงชุดของค่าพร้อมกับผู้ประกอบการที่ใช้สองค่าจากชุดนั้นและสร้างอีกค่าจากชุดนั้น ดังนั้นจำนวนเต็มภายใต้การเพิ่มจึงเป็นกลุ่มตัวอย่างเช่น - +ผู้ประกอบการรวมสอง int เพื่อสร้าง int อื่น

คุณยังสามารถกำหนด semigroup ได้มากกว่าชุดของ "แผนที่ทั้งหมดที่มีประเภทของคีย์และค่าที่กำหนด" ตราบใดที่คุณสามารถดำเนินการบางอย่างที่รวมสองแผนที่เพื่อสร้างแผนที่ใหม่ซึ่งเป็นการรวมกันของสองอย่างใด ปัจจัยการผลิต

หากไม่มีคีย์ที่ปรากฏในแผนที่ทั้งสองนี่เป็นเรื่องเล็กน้อย หากคีย์เดียวกันมีอยู่ในทั้งสองแผนที่เราต้องรวมค่าสองค่าที่คีย์แมปเข้าด้วยกัน อืมเราไม่ได้เพิ่งอธิบายผู้ประกอบการที่รวมสองหน่วยงานประเภทเดียวกันหรือไม่ นี่คือเหตุผลว่าทำไมใน Scalaz จึงมี semigroup สำหรับMap[K, V]ถ้าหาก Semigroup สำหรับVมีอยู่ - Vsemigroup ของมีการใช้เพื่อรวมค่าจากแผนที่สองแผนที่ที่กำหนดให้กับคีย์เดียวกัน

ดังนั้นเพราะIntเป็นค่าชนิดที่นี่ที่ "ชน" ที่1สำคัญได้รับการแก้ไขโดยการเพิ่มจำนวนเต็มของค่าทั้งสองแมป (เป็นที่สิ่งที่ Int ของกึ่งกลุ่มผู้ประกอบการไม่) 100 + 9จึง หากค่านั้นเป็น Strings การชนกันจะส่งผลให้มีการต่อสายอักขระของค่าที่แมปสองค่า (อีกครั้งเพราะนั่นคือสิ่งที่ตัวดำเนินการ semigroup สำหรับ String ทำ)

(และน่าสนใจเนื่องจากการต่อสตริงไม่ใช่การสลับ - นั่นคือ"a" + "b" != "b" + "a"- การดำเนินการกลุ่ม semigroup ที่ได้นั้นไม่เหมือนกันดังนั้นจึงmap1 |+| map2แตกต่างจากmap2 |+| map1ในกรณี String แต่ไม่ใช่ในกรณี Int)


37
ยอดเยี่ยม! ตัวอย่างแรกในทางปฏิบัติที่scalazทำให้รู้สึก
soc

5
ไม่ได้ล้อเล่น! หากคุณเริ่มมองหามัน ... มันอยู่ทุกที่ หากต้องการอ้างถึงผู้แต่งรายละเอียดและข้อมูลจำเพาะ erric torrebone 2: "ก่อนอื่นคุณจะได้เรียนรู้ตัวเลือกและคุณเริ่มเห็นมันทุกที่จากนั้นคุณเรียนรู้การใช้งานและมันเป็นสิ่งเดียวกัน ถัดไปเป็นแนวคิดการทำงานที่มากยิ่งขึ้น และสิ่งเหล่านั้นช่วยให้คุณจัดโครงสร้างโค้ดของคุณและแก้ปัญหาได้
AndreasScheinert

4
ที่จริงแล้วฉันกำลังมองหาตัวเลือกเป็นเวลาห้าปีเมื่อในที่สุดฉันก็พบสกาล่า ความแตกต่างระหว่างการอ้างอิงวัตถุ Java ที่อาจเป็นโมฆะและที่ไม่สามารถ (เช่นระหว่างAและOption[A]) มีขนาดใหญ่มากฉันไม่อยากจะเชื่อว่าพวกเขาเป็นชนิดเดียวกันจริงๆ ฉันเพิ่งเริ่มดู Scalaz ฉันไม่แน่ใจว่าฉันฉลาดพอ ...
Malvolio

1
มีตัวเลือกสำหรับ Java ด้วยดูหน้าที่ Java ไม่มีความกลัวใด ๆ การเรียนรู้เป็นเรื่องสนุก และการเขียนโปรแกรมฟังก์ชั่นไม่ได้สอนสิ่งใหม่ (เท่านั้น) แต่ให้ความช่วยเหลือแก่โปรแกรมเมอร์ในการจัดเตรียมคำศัพท์คำศัพท์เพื่อแก้ไขปัญหา คำถาม OP เป็นตัวอย่างที่สมบูรณ์แบบ แนวคิดของ Semigroup นั้นง่ายมากคุณใช้มันทุกวันเพื่อพูดถึงเช่น Strings พลังที่แท้จริงจะปรากฏขึ้นหากคุณระบุสิ่งที่เป็นนามธรรมนี้ตั้งชื่อมันและในที่สุดก็นำไปใช้กับประเภทอื่น ๆ แล้วเพียงแค่สตริง
AndreasScheinert

1
เป็นไปได้อย่างไรที่จะส่งผลให้ 1 -> (100 + 9) คุณช่วยโชว์ "stack trace" ให้ฉันดูได้ไหม ขอบคุณ. 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)
dividebyzero

ฉันทำอะไรบางอย่างที่แตกต่างกันไปก่อนหน้านี้ แต่นี่คือรุ่นของสิ่งที่คุณทำแทนที่แผนที่สำหรับformap1 ++ (สำหรับ ((k, v) <- map2) ให้ผลผลิต k -> (v + map1.getOrElse (k, 0 )))
dividebyzero

1
@ Jus12 - ไม่มีลำดับ .ความสำคัญสูงกว่า++; คุณอ่านเป็นmap1 ++ map2.map{...} map1 ++ (map2 map {...})ดังนั้นวิธีหนึ่งที่คุณแมปmap1องค์ประกอบของและวิธีอื่นที่คุณทำไม่ได้
Rex Kerr

1
@matt - Scalaz จะทำมันแล้วฉันจะบอกว่า "ห้องสมุดที่มีอยู่ทำไปแล้ว"
Rex Kerr


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วิธีการอยู่บน performant ++มากขึ้นเฉลี่ยกว่าการทำสำรวจเส้นทางและการฟื้นฟูแผนที่กัญชาใหม่เปลี่ยนรูปจากรอยขีดข่วนหรือ


1
ณ ตอนนี้มันเป็นเพียง Hashmap ที่ไม่เปลี่ยนรูปไม่ได้ Hashmap ที่ไม่แน่นอน
Kevin Wheeler

2
นี่เป็นเรื่องน่ารำคาญที่พวกเขามีเพียงเพื่อให้ HashMaps ซื่อสัตย์
Johan S

ฉันไม่สามารถรวบรวมสิ่งนี้ได้ดูเหมือนว่าประเภทที่ยอมรับเป็นแบบส่วนตัวดังนั้นฉันจึงไม่สามารถส่งผ่านฟังก์ชันพิมพ์ที่ตรงกัน
Ryan The Leach

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 group คุณสามารถทำได้อย่างง่ายดาย

จะมีลักษณะเช่น:

  import scalaz.Scalaz._
  map1 |+| map2

11
คุณต้องใส่รายละเอียดเพิ่มเติมในคำตอบของคุณโดยเฉพาะอย่างยิ่งรหัสการติดตั้งบางส่วน ทำเช่นนี้สำหรับคำตอบที่คล้ายกันอื่น ๆ ที่คุณโพสต์และปรับแต่งแต่ละคำตอบสำหรับคำถามเฉพาะที่ถูกถาม Rule of Thumb:ผู้ถามควรได้รับประโยชน์จากคำตอบของคุณโดยไม่ต้องคลิกที่ลิงค์บล็อก
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)

import cats.implicits._จี๊ด, นำเข้าimport cats.instances.map._ import cats.instances.int._ import cats.syntax.semigroup._verbose ไม่มาก ...
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)

นี้:

  • เชื่อมสองแผนที่เข้าด้วยกันเป็นลำดับของ tuples ( List((1,9), (2,20), (1,100), (3,300))) สำหรับกระชับ, map2เป็นปริยายแปลงไปSeqปรับให้เข้ากับประเภทของmap1.toSeq- แต่คุณสามารถเลือกที่จะทำให้มันชัดเจนโดยใช้map2.toSeq,

  • groupองค์ประกอบขึ้นอยู่กับส่วน tuple แรกของพวกเขา (ส่วนกลุ่มของกลุ่ม MapReduce)

  • mapจัดกลุ่มค่าไปยังส่วน tuple ที่สองของพวกเขา (ส่วนที่เป็นแผนที่ของการย่อแผนที่กลุ่ม)

  • reduces แมปค่า ( _+_) จากข้อสรุปพวกเขา (ลดส่วนหนึ่งของ groupMap ลด )


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วิธีการโดยปริยาย


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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.