Modo elegante per invertire una mappa in Scala


97

Imparando Scala attualmente e avevo bisogno di invertire una mappa per fare alcune ricerche di valori invertiti-> chiave. Stavo cercando un modo semplice per farlo, ma ho trovato solo:

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

Qualcuno ha un approccio più elegante?

Risposte:


174

Supponendo che i valori siano unici, funziona:

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

Su Scala 2.8, invece, è più semplice:

origMap.map(_.swap)

Essere in grado di farlo è parte del motivo per cui Scala 2.8 ha una nuova libreria di collezioni.


1
Attento! Puoi perdere valori con questa soluzione. Per esempio Map(1 -> "A", 2 -> "B", 3 -> "B").map(_.swap)comportaMap(A -> 1, B -> 3)
dev-nullo

1
@ dev-null Mi dispiace, ma il tuo esempio non rientra nell'ipotesi richiesta.
Daniel C. Sobral

Sono d'accordo. Scusa, l'ho trascurato.
dev-null

45

Matematicamente, la mappatura potrebbe non essere invertibile (iniettiva), ad es. Da Map[A,B], non puoi ottenere Map[B,A], ma piuttosto ottieni Map[B,Set[A]], perché potrebbero esserci chiavi diverse associate agli stessi valori. Quindi, se sei interessato a conoscere tutte le chiavi, ecco il codice:

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

2
.map(_._1)sarebbe più leggibile come solo.keys
cheezsteak

Ora grazie a te, anche qualche carattere più corto. La differenza ora è che ottieni Sets invece di Lists come prima.
Rok Kralj

1
Fai attenzione quando usi .mapValuesperché restituisce una vista. Di tanto in tanto, questo è quello che vuoi, ma se non stai attento può consumare molta memoria e CPU. Per forzarlo in una mappa, puoi farlo m.groupBy(_._2).mapVaues(_.keys).map(identity)o potresti sostituire la chiamata a .mapValues(_.keys)con .map { case (k, v) => k -> v.keys }.
Mark T.

10

Puoi evitare le cose ._1 durante l'iterazione in diversi modi.

Ecco un modo. Questo utilizza una funzione parziale che copre l'unico caso importante per la mappa:

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

Ecco un altro modo:

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

L'iterazione della mappa chiama una funzione con una tupla a due elementi e la funzione anonima vuole due parametri. Function.tupled effettua la traduzione.


6

Sono venuto qui cercando un modo per invertire una mappa di tipo Map [A, Seq [B]] in Map [B, Seq [A]], dove ogni B nella nuova mappa è associato a ogni A nella vecchia mappa per quale la B era contenuta nella sequenza associata di A.

Ad esempio,
Map(1 -> Seq("a", "b"), 2-> Seq("b", "c"))
invertirebbe in
Map("a" -> Seq(1), "b" -> Seq(1, 2), "c" -> Seq(2))

Ecco la mia soluzione:

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

dove oldMap è di tipo Map[A, Seq[B]]e newMap è di tipoMap[B, Seq[A]]

I foldLeft nidificati mi fanno rabbrividire un po ', ma questo è il modo più semplice che ho trovato per realizzare questo tipo di inversione. Qualcuno ha una soluzione più pulita?


soluzione molto bella! @Rok, la sua soluzione è in qualche modo diversa dalla vostra un po 'credo perché si trasforma: Map[A, Seq[B]]da Map[B, Seq[A]]dove i vostri trasnforms soluzione Map[A, Seq[B]]a Map[Seq[B], Seq[A]].
YH

1
In quel caso, senza due pieghe annidate e possibili più performanti:a.toSeq.flatMap { case (a, b) => b.map(_ -> a) }.groupBy(_._2).mapValues(_.map(_._1))
Rok Kralj

6

OK, questa è una domanda molto vecchia con molte buone risposte, ma ho costruito il coltellino svizzero definitivo, universale, Mapinverter e questo è il posto giusto per pubblicarlo.

In realtà sono due inverter. Uno per i singoli elementi di valore ...

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

... e un altro, abbastanza simile, per le collezioni di valore.

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

utilizzo:

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

Preferirei avere entrambi i metodi nella stessa classe implicita, ma più tempo passavo a esaminarlo più appariva problematico.


3

Puoi invertire una mappa usando:

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

Il problema con questo approccio è che se i tuoi valori, che ora sono diventati le chiavi hash nella tua mappa, non sono univoci, lascerai cadere i valori duplicati. Illustrare:

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)

Per evitare ciò, puoi prima convertire la tua mappa in un elenco di tuple, quindi invertire, in modo da non rilasciare valori duplicati:

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

1

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

Tieni presente che i valori duplicati verranno sovrascritti dall'ultima aggiunta alla mappa:

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)

1

Iniziando Scala 2.13, per scambiare chiave / valori senza perdere le chiavi associate agli stessi valori, possiamo usare Mapil nuovo metodo groupMap , che (come suggerisce il nome) è l'equivalente di un groupBye un mapping su elementi raggruppati.

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

Questo:

  • groups elementi basati sulla loro seconda parte tupla ( _._2) (parte del gruppo della mappa del gruppo )

  • maps raggruppa gli elementi prendendo la loro prima tupla part ( _._1) (map part of group Map )

Questo può essere visto come una versione one-pass di map.groupBy(_._2).mapValues(_.map(_._1)).


Peccato che non si trasformi Map[K, C[V]]in Map[V, C[K]].
jwvh

0
  1. Inverse è un nome migliore per questa operazione che reverse (come in "inverso di una funzione matematica")

  2. Faccio spesso questa trasformazione inversa non solo sulle mappe ma su altre raccolte (incluso Seq). Trovo sia meglio non limitare la definizione della mia operazione inversa alle mappe uno-a-uno. Ecco la definizione con cui opero per le mappe (suggerisci miglioramenti alla mia implementazione).

    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
    }

Se si tratta di una mappa uno-a-uno, si ottengono elenchi singleton che possono essere banalmente testati e trasformati in una mappa [B, A] invece che in una mappa [B, List [A]].


0

Possiamo provare a utilizzare questa foldLeftfunzione che si prenderà cura delle collisioni e invertirà la mappa in un unico attraversamento.

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))
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.