Elegant way to invert a map in Scala

前端 未结 10 555
慢半拍i
慢半拍i 2020-12-02 13:49

Learning Scala currently and needed to invert a Map to do some inverted value->key lookups. I was looking for a simple way to do this, but came up with only:



        
相关标签:
10条回答
  • 2020-12-02 14:39

    Starting Scala 2.13, in order to swap key/values without loosing keys associated to same values, we can use Maps new groupMap method, which (as its name suggests) is an equivalent of a groupBy and a mapping over grouped items.

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

    This:

    • groups elements based on their second tuple part (_._2) (group part of groupMap)

    • maps grouped items by taking their first tuple part (_._1) (map part of groupMap)

    This can be seen as a one-pass version of map.groupBy(_._2).mapValues(_.map(_._1)).

    0 讨论(0)
  • 2020-12-02 14:47

    You could invert a map using:

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

    The problem with this approach is that if your values, which have now become the hash keys in your map, are not unique you will drop the duplicate values. To illustrate:

    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)
    

    To avoid this you can convert your map to a list of tuples first, then invert, so that you don't drop any duplicate values:

    scala> val i = m.toList.map({ case(k , v) => v -> k})
    i: List[(Int, String)] = List((1,a), (2,b), (3,c), (1,d))
    
    0 讨论(0)
  • 2020-12-02 14:48

    We can try using this foldLeft function that will take care of collisions and invert the map in single traversal.

    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))
    
    0 讨论(0)
  • 2020-12-02 14:50
    1. Inverse is a better name for this operation than reverse (as in "inverse of a mathematical function")

    2. I often do this inverse transformation not only on maps but on other (including Seq) collections. I find it best not to limit the definition of my inverse operation to one-to-one maps. Here's the definition I operate with for maps (please suggest improvements to my implementation).

      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
      }
      

    If it's a one-to-one map, you end up with singleton lists which can be trivially tested and transformed to a Map[B,A] rather than Map[B,List[A]].

    0 讨论(0)
提交回复
热议问题