Cách tốt nhất để hợp nhất hai bản đồ và tổng hợp các giá trị của cùng một khóa?


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

Tôi muốn hợp nhất chúng và tổng hợp các giá trị của cùng một khóa. Vì vậy, kết quả sẽ là:

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

Bây giờ tôi có 2 giải pháp:

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

Nhưng tôi muốn biết nếu có bất kỳ giải pháp tốt hơn.


Dễ nhất làmap1 ++ map2
Seraf

3
@Seraf Điều đó thực sự chỉ đơn giản là "hợp nhất" các bản đồ, bỏ qua các bản sao thay vì tổng hợp các giá trị của chúng.
Zeynep Akkalyoncu Yilmaz

@ZeynepAkkalyoncuYilmaz phải đọc câu hỏi tốt hơn, xấu hổ
Seraf

Câu trả lời:


142

Scalaz có khái niệm về một Semigroup nắm bắt những gì bạn muốn làm ở đây và dẫn đến giải pháp ngắn nhất / sạch nhất:

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)

Cụ thể, toán tử nhị phân để Map[K, V]kết hợp các khóa của bản đồ, gấp Vtoán tử nửa nhóm trên bất kỳ giá trị trùng lặp nào. Nhóm bán kết chuẩn để Intsử dụng toán tử bổ sung, do đó bạn có được tổng giá trị cho mỗi khóa trùng lặp.

Chỉnh sửa : Chi tiết hơn một chút, theo yêu cầu của người dùng482745.

Về mặt toán học, một nửa nhóm chỉ là một tập hợp các giá trị, cùng với một toán tử lấy hai giá trị từ tập đó và tạo ra một giá trị khác từ tập đó. Vì vậy, các số nguyên dưới đây là một nửa nhóm, ví dụ - +toán tử kết hợp hai số nguyên để tạo một số nguyên khác.

Bạn cũng có thể xác định một nửa nhóm trên tập hợp "tất cả các bản đồ với loại khóa và loại giá trị nhất định", miễn là bạn có thể đưa ra một số thao tác kết hợp hai bản đồ để tạo ra một bản đồ mới bằng cách nào đó kết hợp cả hai bản đồ đầu vào.

Nếu không có phím nào xuất hiện trong cả hai bản đồ thì đây là chuyện nhỏ. Nếu cùng một khóa tồn tại trong cả hai bản đồ, thì chúng ta cần kết hợp hai giá trị mà khóa ánh xạ tới. Hmm, chúng ta vừa mô tả một toán tử kết hợp hai thực thể cùng loại phải không? Đây là lý do tại sao trong Scalaz một nhóm bán kết Map[K, V]tồn tại khi và chỉ khi một nhóm bán kết Vtồn tại - Vnhóm bán kết được sử dụng để kết hợp các giá trị từ hai bản đồ được gán cho cùng một khóa.

Vì vậy, vì Intlà loại giá trị ở đây, "xung đột" trên 1khóa được giải quyết bằng phép cộng số nguyên của hai giá trị được ánh xạ (như đó là điều mà toán tử semigroup thực hiện), do đó 100 + 9. Nếu các giá trị là Chuỗi, một xung đột sẽ dẫn đến kết hợp chuỗi của hai giá trị được ánh xạ (một lần nữa, vì đó là điều mà toán tử semigroup cho Chuỗi thực hiện).

(Và thật thú vị, vì nối chuỗi không phải là giao hoán - nghĩa là "a" + "b" != "b" + "a"- hoạt động semigroup kết quả cũng không. Vì vậy, map1 |+| map2khác với map2 |+| map1trong trường hợp Chuỗi, nhưng không phải trong trường hợp Int.)


37
Xuất sắc! Ví dụ thực tế đầu tiên nơi scalazcó ý nghĩa.
soc

5
Không đua đâu! Nếu bạn bắt đầu tìm kiếm nó ... nó ở khắp mọi nơi. Để trích dẫn tác giả erric torrebone của thông số kỹ thuật và thông số kỹ thuật2: "Đầu tiên bạn học Tùy chọn và bạn bắt đầu nhìn thấy nó ở mọi nơi. Sau đó, bạn học Ứng dụng và đó là điều tương tự. Tiếp theo?" Tiếp theo là các khái niệm chức năng thậm chí nhiều hơn. Và những thứ đó giúp bạn cấu trúc mã của bạn và giải quyết vấn đề độc đáo.
AndreasScheinert

4
Trên thực tế, tôi đã tìm kiếm Lựa chọn trong năm năm khi cuối cùng tôi tìm thấy Scala. Sự khác biệt giữa một tham chiếu đối tượng Java có thể là null và một tham chiếu không thể (nghĩa là giữa AOption[A]) là rất lớn, tôi không thể tin rằng chúng thực sự cùng loại. Tôi chỉ bắt đầu nhìn vào Scalaz. Tôi không chắc mình đủ thông minh ...
Malvolio

1
Cũng có Tùy chọn cho Java, xem Java Chức năng. Đừng có sợ hãi, học tập là niềm vui. Và lập trình chức năng không dạy cho bạn những điều mới (chỉ) mà thay vào đó cung cấp cho bạn trợ giúp lập trình viên với việc cung cấp các thuật ngữ, từ vựng để giải quyết vấn đề. Câu hỏi OP là một ví dụ hoàn hảo. Khái niệm về Semigroup rất đơn giản, bạn sử dụng nó mỗi ngày, ví dụ như Chuỗi. Sức mạnh thực sự xuất hiện nếu bạn xác định sự trừu tượng này, đặt tên cho nó và cuối cùng áp dụng nó cho các loại khác sau đó chỉ là Chuỗi.
AndreasScheinert

1
Làm thế nào có thể nó sẽ dẫn đến 1 -> (100 + 9)? Bạn có thể vui lòng chỉ cho tôi "dấu vết ngăn xếp"? Cám ơn. PS: Tôi đang hỏi ở đây để làm cho câu trả lời rõ ràng hơn.
dùng482745

152

Câu trả lời ngắn nhất mà tôi biết chỉ sử dụng thư viện chuẩn là

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

34
Giải pháp tốt đẹp. Tôi muốn thêm gợi ý, ++thay thế bất kỳ (k, v) nào từ bản đồ ở bên trái của ++(ở đây map1) bởi (k, v) từ bản đồ bên phải, nếu (k, _) đã tồn tại ở bên trái bản đồ bên (ở đây map1), ví dụMap(1->1) ++ Map(1->2) results in Map(1->2)
Lutz

Một loại phiên bản gọn gàng hơn: for ((k, v) <- (aa ++ bb)) mang lại k -> (if ((aa chứa k) && (bb chứa k)) aa (k) + v khác v)
splititherzero

Tôi đã làm một số khác biệt trước đây, nhưng đây là phiên bản của những gì bạn đã làm, thay thế bản đồ cho formap1 ++ (cho ((k, v) <- map2) mang lại k -> (v + map1.getOrElse (k, 0 )))
splititherzero

1
@ Jus12 - Số .có quyền ưu tiên cao hơn ++; bạn đọc map1 ++ map2.map{...}như map1 ++ (map2 map {...}). Vì vậy, một cách bạn ánh xạ map1các yếu tố và cách khác bạn không.
Rex Kerr

1
@matt - Scalaz đã làm điều đó, vì vậy tôi nói "một thư viện hiện có đã làm điều đó".
Rex Kerr

48

Giải pháp nhanh chóng:

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

41

Chà, bây giờ trong thư viện scala (ít nhất là trong 2.10) có một thứ bạn muốn - hàm hợp nhất . NHƯNG nó chỉ được trình bày trong HashMap chứ không phải trong Bản đồ. Nó hơi khó hiểu. Ngoài ra, chữ ký rất cồng kềnh - không thể tưởng tượng được tại sao tôi cần một khóa hai lần và khi nào tôi cần sản xuất một cặp với một khóa khác. Nhưng tuy nhiên, nó hoạt động và sạch hơn nhiều so với các giải pháp "bản địa" trước đây.

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

Ngoài ra trong scaladoc đã đề cập rằng

Các mergedphương pháp là trên performant hơn trung bình so với làm một traversal và xây dựng lại một bản đồ băm bất biến mới từ đầu, hoặc ++.


1
Ngay bây giờ, nó chỉ có trong Hashmap bất biến, không phải là Hashmap có thể thay đổi.
Kevin Wheeler

2
Điều này khá khó chịu khi họ chỉ có điều đó để HashMaps thành thật.
Johan S

Tôi không thể biên dịch nó, có vẻ như kiểu mà nó chấp nhận là riêng tư, vì vậy tôi không thể chuyển vào một hàm được gõ phù hợp.
Ryan The Leach

2
Có vẻ như một cái gì đó đã thay đổi trong phiên bản 2.11. Kiểm tra 2.10 scaladoc - scala-lang.org/api/2.10.1/ Khăn Có một chức năng thông thường. Nhưng trong 2.11 nó MergeFunction.
Mikhail Golubtsov 8/07/2015

Tất cả những gì đã thay đổi trong 2.11 là việc giới thiệu một bí danh loại cho loại chức năng cụ thể nàyprivate type MergeFunction[A1, B1] = ((A1, B1), (A1, B1)) => (A1, B1)
EthanP

14

Điều này có thể được thực hiện như một Monoid chỉ với Scala đơn giản. Đây là một thực hiện mẫu. Với phương pháp này, chúng ta có thể hợp nhất không chỉ 2, mà là một danh sách các bản đồ.

// Monoid trait

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

Việc thực hiện dựa trên Bản đồ của đặc điểm Monoid hợp nhất hai bản đồ.

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
}

Bây giờ, nếu bạn có một danh sách các bản đồ cần được hợp nhất (trong trường hợp này, chỉ có 2), nó có thể được thực hiện như dưới đây.

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

5

Tôi đã viết một bài blog về điều này, kiểm tra xem nó:

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

về cơ bản sử dụng nhóm scalaz bạn có thể đạt được điều này khá dễ dàng

sẽ trông giống như:

  import scalaz.Scalaz._
  map1 |+| map2

11
Bạn cần đặt chi tiết hơn một chút trong câu trả lời của bạn, tốt nhất là một số mã thực hiện. Làm điều này cũng cho các câu trả lời tương tự khác mà bạn đã đăng và điều chỉnh từng câu trả lời cho câu hỏi cụ thể đã được hỏi. Nguyên tắc chung: Người hỏi sẽ có thể hưởng lợi từ câu trả lời của bạn mà không cần nhấp vào liên kết blog.
Robert Harvey

5

Bạn cũng có thể làm điều đó với Mèo .

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._. Nhập khẩu import cats.instances.map._ import cats.instances.int._ import cats.syntax.semigroup._không dài dòng hơn ...
St.Antario 14/12/18

@ St.Antario cách thực sự được khuyến nghị là chỉ cóimport cats.implicits._
Artsiom Miklushou

Được giới thiệu bởi ai? Đưa tất cả (hầu hết trong số đó là các trường hợp không sử dụng) vào phạm vi làm phức tạp cuộc sống của nhà soạn nhạc. Và bên cạnh đó, nếu người ta không cần, hãy nói, ví dụ ứng dụng tại sao họ lại mang nó đến đó?
St.Antario

4

Bắt đầu Scala 2.13, một giải pháp khác chỉ dựa trên thư viện tiêu chuẩn bao gồm thay thế groupBymột phần giải pháp của bạn bằng groupMapReduce(như tên gọi của nó) tương đương với groupBybước tiếp theo mapValuesvà bước giảm:

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

Điều này:

  • Nối hai bản đồ thành một chuỗi các bộ dữ liệu ( List((1,9), (2,20), (1,100), (3,300))). Để đơn giản, map2được chuyển đổi hoàn toànSeq để thích ứng với loại map1.toSeq- nhưng bạn có thể chọn làm cho nó rõ ràng bằng cách sử dụng map2.toSeq,

  • groupcác phần tử dựa trên phần tuple đầu tiên của chúng (phần nhóm của nhóm MapReduce),

  • maps các giá trị được nhóm thành phần tuple thứ hai của chúng (phần bản đồ của nhóm Map Map ),

  • reduces ánh xạ các giá trị ( _+_) bằng cách tính tổng chúng (giảm một phần của GroupMap Giảm ).


3

Đây là những gì tôi đã kết thúc bằng cách sử dụng:

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

1
Điều đó thực sự không khác biệt đáng kể so với giải pháp đầu tiên do OP đề xuất.
jwvh

2

Câu trả lời của Andrzej Doyle chứa một lời giải thích tuyệt vời về các nhóm bán kết cho phép bạn sử dụng |+| toán tử để nối hai bản đồ và tính tổng các giá trị cho các khóa khớp.

Có nhiều cách một cái gì đó có thể được định nghĩa là một thể hiện của một kiểu chữ và không giống như OP, bạn có thể không muốn tính tổng các khóa của mình một cách cụ thể. Hoặc, bạn có thể muốn hoạt động trên một liên minh chứ không phải là một giao lộ. Scalaz cũng thêm các chức năng bổ sung choMap cho mục đích này:

https://oss.sonatype.org/service/local/repose khu / snapsshots / archive / org / scalaz / scalaz_2.11 / 7.3.0-SRPSPSOT index.html # scalaz.std.MapFifts

Bạn có thể làm

import scalaz.Scalaz._

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

2

Cách nhanh nhất và đơn giản nhất:

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

Bằng cách này, từng yếu tố ngay lập tức được thêm vào bản đồ.

Cách thứ hai ++là:

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

Không giống như cách thứ nhất, Theo cách thứ hai cho mỗi thành phần trong bản đồ thứ hai, một Danh sách mới sẽ được tạo và nối với bản đồ trước.

Các casebiểu hiện ngầm tạo ra một danh sách mới sử dụng unapplyphương pháp.


1

Đây là những gì tôi nghĩ ra ...

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

Sử dụng mẫu typeclass, chúng ta có thể hợp nhất bất kỳ loại Số nào:

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

Sử dụng:

import MapSyntax.MapOps

map1 plus map2

Hợp nhất một chuỗi các bản đồ:

maps.reduce(_ plus _)

0

Tôi đã có một chức năng nhỏ để thực hiện công việc, đó là trong thư viện nhỏ của tôi cho một số chức năng được sử dụng thường xuyên không có trong lib tiêu chuẩn. Nó nên hoạt động cho tất cả các loại bản đồ, có thể thay đổi và không thay đổi, không chỉ HashMaps

Đây là cách sử dụng

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

Và đây là cơ thể

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.