Cara elegan untuk membalikkan peta di Scala

97

Mempelajari Scala saat ini dan diperlukan untuk membalikkan Peta untuk melakukan beberapa pencarian nilai-> kunci. Saya sedang mencari cara sederhana untuk melakukan ini, tetapi hanya menemukan:

(Map() ++ origMap.map(kvp=>(kvp._2->kvp._1)))

Ada yang punya pendekatan yang lebih elegan?

AlexeyMK
sumber

Jawaban:

174

Dengan asumsi nilai unik, ini berfungsi:

(Map() ++ origMap.map(_.swap))

Pada Scala 2.8, bagaimanapun, ini lebih mudah:

origMap.map(_.swap)

Mampu melakukan itu adalah bagian dari alasan mengapa Scala 2.8 memiliki perpustakaan koleksi baru.

Daniel C. Sobral
sumber
1
Cermat! Anda bisa kehilangan nilai dengan solusi ini. Misalnya Map(1 -> "A", 2 -> "B", 3 -> "B").map(_.swap)hasil dalamMap(A -> 1, B -> 3)
dev-null
1
@ dev-null Maaf, tetapi contoh Anda tidak termasuk dalam asumsi yang disyaratkan.
Daniel C. Sobral
Saya setuju. Maaf, saya sudah melewatkannya.
dev-null
45

Secara matematis, pemetaan mungkin tidak dapat dibalik (injektif), misalnya, dari Map[A,B], Anda tidak bisa mendapatkan Map[B,A], tetapi Anda mendapatkan Map[B,Set[A]], karena mungkin ada kunci berbeda yang terkait dengan nilai yang sama. Jadi, jika Anda tertarik untuk mengetahui semua kuncinya, berikut kodenya:

scala> val m = Map(1 -> "a", 2 -> "b", 4 -> "b")
scala> m.groupBy(_._2).mapValues(_.keys)
res0: Map[String,Iterable[Int]] = Map(b -> Set(2, 4), a -> Set(1))
Rok Kralj
sumber
2
.map(_._1)akan lebih mudah dibaca karena hanya.keys
cheezsteak
Sekarang berkat Anda, bahkan beberapa karakter lebih pendek. Perbedaannya sekarang adalah Anda mendapatkan Sets, bukan Lists seperti sebelumnya.
Rok Kralj
1
Hati-hati saat menggunakan .mapValueskarena mengembalikan tampilan. Terkadang, ini yang Anda inginkan, tetapi jika Anda tidak berhati-hati, hal ini dapat menghabiskan banyak memori dan CPU. Untuk memaksanya menjadi peta, Anda bisa melakukannya m.groupBy(_._2).mapVaues(_.keys).map(identity), atau Anda bisa mengganti panggilan ke .mapValues(_.keys)dengan .map { case (k, v) => k -> v.keys }.
Mark T.
10

Anda dapat menghindari barang ._1 sambil melakukan iterasi dengan beberapa cara.

Inilah salah satu caranya. Ini menggunakan fungsi parsial yang mencakup satu-satunya kasus yang penting untuk peta:

Map() ++ (origMap map {case (k,v) => (v,k)})

Berikut cara lain:

import Function.tupled        
Map() ++ (origMap map tupled {(k,v) => (v,k)})

Iterasi peta memanggil fungsi dengan dua elemen tupel, dan fungsi anonim menginginkan dua parameter. Function.tupled membuat terjemahan.

Lee Mighdoll
sumber
6

Saya datang ke sini mencari cara untuk membalikkan Peta tipe Peta [A, Seq [B]] ke Peta [B, Seq [A]], di mana setiap B di peta baru dikaitkan dengan setiap A di peta lama untuk yang B terkandung dalam urutan terkait A.

Misalnya,
Map(1 -> Seq("a", "b"), 2-> Seq("b", "c"))
akan terbalik menjadi
Map("a" -> Seq(1), "b" -> Seq(1, 2), "c" -> Seq(2))

Inilah solusi saya:

val newMap = oldMap.foldLeft(Map[B, Seq[A]]().withDefaultValue(Seq())) {
  case (m, (a, bs)) => bs.foldLeft(m)((map, b) => map.updated(b, m(b) :+ a))
}

di mana oldMap bertipe Map[A, Seq[B]]dan newMap bertipeMap[B, Seq[A]]

FoldLefts bersarang membuat saya sedikit ngeri, tetapi ini adalah cara paling mudah yang dapat saya temukan untuk mencapai jenis inversi ini. Ada yang punya solusi yang lebih bersih?

Eddie Carlson
sumber
solusi yang sangat bagus! @Rok, solusi nya agak berbeda dari Anda memagut saya pikir karena ia berubah: Map[A, Seq[B]]untuk Map[B, Seq[A]]mana trasnforms solusi Anda Map[A, Seq[B]]untuk Map[Seq[B], Seq[A]].
YH
1
Dalam hal ini, tanpa dua lipatan bersarang dan kemungkinan lebih banyak penampil:a.toSeq.flatMap { case (a, b) => b.map(_ -> a) }.groupBy(_._2).mapValues(_.map(_._1))
Rok Kralj
6

Oke, jadi ini adalah pertanyaan yang sangat lama dengan banyak jawaban yang bagus, tapi saya telah membangun Mapinverter , pisau Swiss-Army, yang paling mutakhir , dan ini adalah tempat untuk mempostingnya.

Ini sebenarnya dua inverter. Satu untuk elemen nilai individu ...

//from Map[K,V] to Map[V,Set[K]], traverse the input only once
implicit class MapInverterA[K,V](m :Map[K,V]) {
  def invert :Map[V,Set[K]] =
    m.foldLeft(Map.empty[V, Set[K]]) {
      case (acc,(k, v)) => acc + (v -> (acc.getOrElse(v,Set()) + k))
    }
}

... dan satu lagi, sangat mirip, untuk koleksi nilai.

import scala.collection.generic.CanBuildFrom
import scala.collection.mutable.Builder
import scala.language.higherKinds

//from Map[K,C[V]] to Map[V,C[K]], traverse the input only once
implicit class MapInverterB[K,V,C[_]](m :Map[K,C[V]]
                                     )(implicit ev :C[V] => TraversableOnce[V]) {
  def invert(implicit bf :CanBuildFrom[Nothing,K,C[K]]) :Map[V,C[K]] =
    m.foldLeft(Map.empty[V, Builder[K,C[K]]]) {
      case (acc, (k, vs)) =>
        vs.foldLeft(acc) {
          case (a, v) => a + (v -> (a.getOrElse(v,bf()) += k))
        }
    }.mapValues(_.result())
}

pemakaian:

Map(2 -> Array('g','h'), 5 -> Array('g','y')).invert
//res0: Map(g -> Array(2, 5), h -> Array(2), y -> Array(5))

Map('q' -> 1.1F, 'b' -> 2.1F, 'c' -> 1.1F, 'g' -> 3F).invert
//res1: Map(1.1 -> Set(q, c), 2.1 -> Set(b), 3.0 -> Set(g))

Map(9 -> "this", 8 -> "that", 3 -> "thus", 2 -> "thus").invert
//res2: Map(this -> Set(9), that -> Set(8), thus -> Set(3, 2))

Map(1L -> Iterator(3,2), 5L -> Iterator(7,8,3)).invert
//res3: Map(3 -> Iterator(1, 5), 2 -> Iterator(1), 7 -> Iterator(5), 8 -> Iterator(5))

Map.empty[Unit,Boolean].invert
//res4: Map[Boolean,Set[Unit]] = Map()

Saya lebih suka memiliki kedua metode di kelas implisit yang sama tetapi semakin banyak waktu yang saya habiskan untuk melihatnya semakin bermasalah.

jwvh
sumber
3

Anda bisa membalikkan peta menggunakan:

val i = origMap.map({case(k, v) => v -> k})

Masalah dengan pendekatan ini adalah jika nilai Anda, yang sekarang menjadi kunci hash di peta Anda, tidak unik, Anda akan membuang nilai duplikat. Menggambarkan:

scala> val m = Map("a" -> 1, "b" -> 2, "c" -> 3, "d" -> 1)
m: scala.collection.immutable.Map[String,Int] = Map(a -> 1, b -> 2, c -> 3, d -> 1)

// Notice that 1 -> a is not in our inverted map
scala> val i = m.map({ case(k , v) => v -> k})
i: scala.collection.immutable.Map[Int,String] = Map(1 -> d, 2 -> b, 3 -> c)

Untuk menghindari hal ini, Anda dapat mengonversi peta Anda ke daftar tupel terlebih dahulu, kemudian membalikkan, sehingga Anda tidak menjatuhkan nilai duplikat:

scala> val i = m.toList.map({ case(k , v) => v -> k})
i: List[(Int, String)] = List((1,a), (2,b), (3,c), (1,d))
hohonuuli
sumber
1

Dalam skala REPL:

scala> val m = Map(1 -> "one", 2 -> "two")
m: scala.collection.immutable.Map[Int,java.lang.String] = Map(1 -> one, 2 -> two)

scala> val reversedM = m map { case (k, v) => (v, k) }
reversedM: scala.collection.immutable.Map[java.lang.String,Int] = Map(one -> 1, two -> 2)

Perhatikan bahwa nilai duplikat akan ditimpa dengan tambahan terakhir pada peta:

scala> val m = Map(1 -> "one", 2 -> "two", 3 -> "one")
m: scala.collection.immutable.Map[Int,java.lang.String] = Map(1 -> one, 2 -> two, 3 -> one)

scala> val reversedM = m map { case (k, v) => (v, k) }
reversedM: scala.collection.immutable.Map[java.lang.String,Int] = Map(one -> 3, two -> 2)
yǝsʞǝla
sumber
1

Memulai Scala 2.13, untuk menukar kunci / nilai tanpa kehilangan kunci yang terkait dengan nilai yang sama, kita dapat menggunakan metode groupMapMap baru , yang (seperti yang disarankan namanya) sama dengan a dan ping atas item yang dikelompokkan.groupBymap

Map(1 -> "a", 2 -> "b", 4 -> "b").groupMap(_._2)(_._1)
// Map("b" -> List(2, 4), "a" -> List(1))

Ini:

  • groups elemen berdasarkan tupel kedua mereka part ( _._2) (bagian grup dari peta grup )

  • maps item dikelompokkan dengan mengambil bagian tuple pertama mereka ( _._1) (map bagian dari kelompok Map )

Ini dapat dilihat sebagai versi sekali jalan dari map.groupBy(_._2).mapValues(_.map(_._1)).

Xavier Guihot
sumber
Sayang sekali itu tidak akan berubah Map[K, C[V]]menjadi Map[V, C[K]].
jwvh
0
  1. Pembalikan adalah nama yang lebih baik untuk operasi ini daripada kebalikan (seperti dalam "kebalikan dari fungsi matematika")

  2. Saya sering melakukan transformasi terbalik ini tidak hanya pada peta tetapi pada koleksi lain (termasuk Seq). Menurut saya yang terbaik adalah tidak membatasi definisi operasi inversi saya pada peta satu-ke-satu. Berikut definisi yang saya operasikan untuk peta (harap sarankan perbaikan pada implementasi saya).

    def invertMap[A,B]( m: Map[A,B] ) : Map[B,List[A]] = {
      val k = ( ( m values ) toList ) distinct
      val v = k map { e => ( ( m keys ) toList ) filter { x => m(x) == e } }
      ( k zip v ) toMap
    }

Jika ini adalah peta satu-ke-satu, Anda akan mendapatkan daftar tunggal yang dapat diuji dan ditransformasikan menjadi Peta [B, A] daripada Peta [B, Daftar [A]].

Ashwin
sumber
0

Kita dapat mencoba menggunakan foldLeftfungsi ini yang akan menangani tabrakan dan membalikkan peta dalam traversal tunggal.

scala> def invertMap[A, B](inputMap: Map[A, B]): Map[B, List[A]] = {
     |     inputMap.foldLeft(Map[B, List[A]]()) {
     |       case (mapAccumulator, (value, key)) =>
     |         if (mapAccumulator.contains(key)) {
     |           mapAccumulator.updated(key, mapAccumulator(key) :+ value)
     |         } else {
     |           mapAccumulator.updated(key, List(value))
     |         }
     |     }
     |   }
invertMap: [A, B](inputMap: Map[A,B])Map[B,List[A]]

scala> val map = Map(1 -> 2, 2 -> 2, 3 -> 3, 4 -> 3, 5 -> 5)
map: scala.collection.immutable.Map[Int,Int] = Map(5 -> 5, 1 -> 2, 2 -> 2, 3 -> 3, 4 -> 3)

scala> invertMap(map)
res0: Map[Int,List[Int]] = Map(5 -> List(5), 2 -> List(1, 2), 3 -> List(3, 4))

scala> val map = Map("A" -> "A", "B" -> "A", "C" -> "C", "D" -> "C", "E" -> "E")
map: scala.collection.immutable.Map[String,String] = Map(E -> E, A -> A, B -> A, C -> C, D -> C)

scala> invertMap(map)
res1: Map[String,List[String]] = Map(E -> List(E), A -> List(A, B), C -> List(C, D))
Pavithran Ramachandran
sumber