Елегантен начин за обръщане на карта в Scala

В момента изучавам Scala и трябваше да обърна карта, за да направя някои търсения на обърната стойност->ключ. Търсих прост начин да направя това, но измислих само:

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

Някой има ли по-елегантен подход?


person AlexeyMK    schedule 25.02.2010    source източник


Отговори (10)


Ако приемем, че стойностите са уникални, това работи:

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

В Scala 2.8 обаче е по-лесно:

origMap.map(_.swap)

Възможността да направите това е част от причината, поради която Scala 2.8 има нова библиотека с колекции.

person Daniel C. Sobral    schedule 25.02.2010
comment
Внимателен! Можете да загубите стойности с това решение. Например Map(1 -> "A", 2 -> "B", 3 -> "B").map(_.swap) води до Map(A -> 1, B -> 3) - person dev-null; 04.04.2019
comment
@dev-null Съжалявам, но вашият пример не попада в изискваното предположение. - person Daniel C. Sobral; 02.05.2019
comment
Съгласен съм. Съжалявам, пропуснах това. - person dev-null; 22.05.2019

Математически картографирането може да не е обратимо (инжективно), например от Map[A,B] не можете да получите Map[B,A], а по-скоро получавате Map[B,Set[A]], защото може да има различни ключове, свързани с едни и същи стойности. Така че, ако се интересувате да знаете всички ключове, ето кода:

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))
person Rok Kralj    schedule 14.06.2014
comment
.map(_._1) би било по-четливо като просто .keys - person cheezsteak; 12.02.2016
comment
Сега, благодарение на вас, дори с няколко знака по-кратък. Разликата сега е, че получавате Sets вместо Lists както преди. - person Rok Kralj; 12.02.2016
comment
Бъдете внимателни, когато използвате .mapValues, защото връща изглед. Понякога това е, което искате, но ако не внимавате, може да изразходва много памет и процесор. За да го принудите да влезе в карта, можете да направите m.groupBy(_._2).mapVaues(_.keys).map(identity) или можете да замените извикването на .mapValues(_.keys) с .map { case (k, v) => k -> v.keys }. - person Mark T.; 07.02.2019
comment
.mapValues(_.keySet) е добра опция за получаване на Set, а не Iterable - person devyn; 19.10.2020
comment
Използвайки по-нови версии на Scala, можете да m.groupMap(_._2)(_._1) - person scand1sk; 21.03.2021

Можете да избегнете нещата ._1, докато итерирате по няколко начина.

Ето един начин. Това използва частична функция, която покрива единствения случай, който има значение за картата:

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

Ето още един начин:

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

Итерацията на картата извиква функция с кортеж от два елемента, а анонимната функция иска два параметъра. Function.tupled прави превода.

person Lee Mighdoll    schedule 25.02.2010

Дойдох тук, търсейки начин да обърна Карта от тип Map[A, Seq[B]] към Map[B, Seq[A]], където всяко B в новата карта е свързано с всяко A в старата карта за което B се съдържа в асоциираната последователност на A.

Например
Map(1 -> Seq("a", "b"), 2-> Seq("b", "c"))
би се обърнало към
Map("a" -> Seq(1), "b" -> Seq(1, 2), "c" -> Seq(2))

Ето моето решение:

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

където oldMap е от тип Map[A, Seq[B]], а newMap е от тип Map[B, Seq[A]]

Вложените foldLefts ме карат да настръхвам малко, но това е най-простият начин, който мога да намеря, за да изпълня този тип инверсия. Някой има ли по-чисто решение?

person Eddie Carlson    schedule 05.11.2013
comment
много хубаво решение! @Rok, неговото решение е някак по-различно от твоето според мен, защото той трансформира: Map[A, Seq[B]] в Map[B, Seq[A]], където вашето решение се трансформира Map[A, Seq[B]] в Map[Seq[B], Seq[A]]. - person Y.H.; 20.08.2015
comment
В този случай, без две вложени сгъвания и възможно по-производително: a.toSeq.flatMap { case (a, b) => b.map(_ -> a) }.groupBy(_._2).mapValues(_.map(_._1)) - person Rok Kralj; 20.08.2015

Добре, така че това е много стар въпрос с много добри отговори, но създадох най-добрия, всеобхватен, швейцарски нож, Map инвертор и това е мястото да го публикувам.

Всъщност това са два инвертора. Един за индивидуални стойностни елементи...

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

...и друг, доста подобен, за ценни колекции.

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

употреба:

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

Бих предпочел да имам и двата метода в един и същ имплицитен клас, но колкото повече време прекарвах в разглеждането му, толкова по-проблемно изглеждаше.

person jwvh    schedule 16.07.2018

Можете да обърнете карта, като използвате:

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

Проблемът с този подход е, че ако вашите стойности, които сега са станали хеш ключове във вашата карта, не са уникални, вие ще премахнете дублиращите се стойности. За да илюстрирам:

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)

За да избегнете това, можете първо да преобразувате вашата карта в списък от кортежи, след което да обърнете, така че да не изпускате дублиращи се стойности:

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

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

Имайте предвид, че дублиращите се стойности ще бъдат презаписани от последното добавяне към картата:

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)
person yǝsʞǝla    schedule 09.10.2013

Започвайки от Scala 2.13, за да разменяме ключ/стойности, без да губим ключове, свързани със същите стойности, можем да използваме Maps new groupMap метод, който (както подсказва името му) е еквивалент на groupBy и mapping върху групирани елементи.

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

Това:

  • groups елементи въз основа на тяхната втора част от кортеж (_._2) (групова част от групаКарта)

  • maps групира елементи, като вземе първата им част от кортеж (_._1) (част от картата на групатаКарта)

Това може да се разглежда като версия с едно преминаване на map.groupBy(_._2).mapValues(_.map(_._1)).

person Xavier Guihot    schedule 13.02.2019
comment
Жалко, че няма да трансформира Map[K, C[V]] в Map[V, C[K]]. - person jwvh; 14.02.2019

  1. Обратно е по-добро име за тази операция от обратно (както в "обратно на математическа функция")

  2. Често правя тази обратна трансформация не само на карти, но и на други (включително Seq) колекции. Намирам за най-добре да не ограничавам дефиницията на моята обратна операция до карти едно към едно. Ето дефиницията, с която работя за карти (моля, предложете подобрения в моята реализация).

    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
    }
    

Ако това е карта едно към едно, в крайна сметка получавате единични списъци, които могат да бъдат тривиално тествани и трансформирани в Map[B,A], а не в Map[B,List[A]].

person Ashwin    schedule 19.05.2012

Можем да опитаме да използваме тази foldLeft функция, която ще се погрижи за сблъсъците и ще обърне картата в едно обхождане.

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))
person Pavithran Ramachandran    schedule 29.12.2017