【问题标题】:Scala GroupBy preserving insertion order?Scala GroupBy 保留插入顺序?
【发布时间】:2012-03-24 13:33:26
【问题描述】:

Lists、Maps等中的groupBy方法,在函数之后生成一个Map。

有没有办法使用 groupBy 生成保留插入顺序的 Map(例如 LinkedHashMap)?

我正在使用 for 循环手动插入,但我想知道其中一个有用的已定义函数是否可以帮助我。

提前致谢。

【问题讨论】:

    标签: scala collections map hashmap


    【解决方案1】:

    TraversableLike 上定义的groupBy 会生成immutable.Map,因此您不能让此方法生成其他内容。

    已经保留了每个条目中元素的顺序,但没有保留键的顺序。键是提供的函数的结果,因此它们实际上没有顺序。

    如果您想根据特定键的第一次出现来下订单,这里有一个关于如何做的草图。假设我们想按值 / 2 对整数进行分组:

    val m = List(4, 0, 5, 1, 2, 6, 3).zipWithIndex groupBy (_._1 / 2)
    val lhm = LinkedHashMap(m.toSeq sortBy (_._2.head._2): _*)
    lhm mapValues (_ map (_._1))
    // Map(2 -> List(4, 5), 0 -> List(0, 1), 1 -> List(2, 3), 3 -> List(6))
    // Note order of keys is same as first occurrence in original list
    

    【讨论】:

    • “每个条目中元素的顺序已经被保留”,这有保证吗? API 文档中似乎并没有多说。
    • @Mortimer 如果 API 文档没有这么说,那么我想理论上不能保证(尽管文档通常很差)。元素的顺序只对Seqs 有意义,而这个方法对所有Traversables 都是通用的,但是由于实现使用了一个for 表达式来遍历元素,所以对于Seqs 总是如此.
    • 对,我就是这么想的。我必须确保他们继续订购。谢谢。
    • @Mortimer 我的意思是,如果你愿意,可以包含一个单元测试,但这不是你需要在运行时检查的东西
    • 大多数涉及元组的例子是最难阅读和理解的,包括这个。我投了赞成票,因为它有效:)
    【解决方案2】:

    以下将为您提供一个 groupByOrderedUnique 方法,该方法的行为与您所寻求的一样。它还添加了一个 groupByOrdered 来保留其他人在 cmets 中要求的重复项。

    import collection.immutable.ListSet
    import collection.mutable.{LinkedHashMap => MMap, Builder}
    
    implicit class GroupByOrderedImplicitImpl[A](val t: Traversable[A]) extends AnyVal {
      def groupByOrderedUnique[K](f: A => K): Map[K, ListSet[A]] =
        groupByGen(ListSet.newBuilder[A])(f)
    
      def groupByOrdered[K](f: A => K): Map[K, List[A]] =
        groupByGen(List.newBuilder[A])(f)
    
      def groupByGen[K, C[_]](makeBuilder: => Builder[A, C[A]])(f: A => K): Map[K, C[A]] = {
        val map = MMap[K, Builder[A, C[A]]]()
        for (i <- t) {
          val key = f(i)
          val builder = map.get(key) match {
            case Some(existing) => existing
            case None =>
              val newBuilder = makeBuilder
              map(key) = newBuilder
              newBuilder
          }
          builder += i
        }
        map.mapValues(_.result).toMap
      }
    }
    

    当我使用该代码时:

    import GroupByOrderedImplicit._
      
    val range = 0.until(40)
    val in = range ++ range.reverse
      
    println("With dupes:")
    in.groupByOrdered(_ % 10).toList.sortBy(_._1).foreach(println)
      
    println("\nUnique:")
    in.groupByOrderedUnique(_ % 10).toList.sortBy(_._1).foreach(println)
    

    我得到以下输出:

    With dupes:
    (0,List(0, 10, 20, 30, 30, 20, 10, 0))
    (1,List(1, 11, 21, 31, 31, 21, 11, 1))
    (2,List(2, 12, 22, 32, 32, 22, 12, 2))
    (3,List(3, 13, 23, 33, 33, 23, 13, 3))
    (4,List(4, 14, 24, 34, 34, 24, 14, 4))
    (5,List(5, 15, 25, 35, 35, 25, 15, 5))
    (6,List(6, 16, 26, 36, 36, 26, 16, 6))
    (7,List(7, 17, 27, 37, 37, 27, 17, 7))
    (8,List(8, 18, 28, 38, 38, 28, 18, 8))
    (9,List(9, 19, 29, 39, 39, 29, 19, 9))
    
    Unique:
    (0,ListSet(0, 10, 20, 30))
    (1,ListSet(1, 11, 21, 31))
    (2,ListSet(2, 12, 22, 32))
    (3,ListSet(3, 13, 23, 33))
    (4,ListSet(4, 14, 24, 34))
    (5,ListSet(5, 15, 25, 35))
    (6,ListSet(6, 16, 26, 36))
    (7,ListSet(7, 17, 27, 37))
    (8,ListSet(8, 18, 28, 38))
    (9,ListSet(9, 19, 29, 39))
    

    【讨论】:

    • 虽然不能替换 groupBy...但可能需要调用 toList 值,即 LinkedHashSet
    • 您的实现存在问题,即您的密钥已设置,因此它会删除重复值...
    • 你为什么要扩展AnyVal
    • @St.Antario,我扩展了AnyVal,以便在不分配的情况下调用隐式包装类的方法。这个页面很好地描述了“类型丰富”的基本原理。 ivanyu.me/blog/2014/12/14/value-classes-in-scala
    • 对此的一种变体是使用ListBuffer 代替LinkedHashSet - 使用map(key) = map(key) :+ i 进行应用。根据groupBy 的标准行为,这允许结果中出现重复值
    【解决方案3】:

    这是一个没有地图的:

    def orderedGroupBy[T, P](seq: Traversable[T])(f: T => P): Seq[(P, Traversable[T])] = {
       @tailrec
       def accumulator(seq: Traversable[T], f: T => P, res: List[(P, Traversable[T])]): Seq[(P, Traversable[T])] = seq.headOption match {
         case None => res.reverse
         case Some(h) => {
           val key = f(h)
           val subseq = seq.takeWhile(f(_) == key)
           accumulator(seq.drop(subseq.size), f, (key -> subseq) :: res)
         }
       }
       accumulator(seq, f, Nil)
     }
    

    如果您只需要按顺序访问结果(无随机访问)并且希望避免创建和使用 Map 对象的开销,这可能会很有用。注意:我没有将性能与其他选项进行比较,实际上可能更糟。

    编辑:为了清楚起见;这假设您的输入已经按组键排序。我的用例是SELECT ... ORDER BY

    【讨论】:

    • 这不会处理序列中的乱序元素。即 (A,A,A,B,B,B,C,C,C) 将按预期分组,而不是 (A,B,C,A,B,C,A,B,C)。
    • 好点马格纳斯;这就是这个函数的重点(我希望也是这个问题)。但它需要明确说明。
    【解决方案4】:

    这会在 ScalaMeter 上产生更好的结果,尽管该解决方案与实际的 scala groupBy 非常相似

        ::Benchmark Range.GroupBy::
        cores: 8
        hostname: xxxxx-MacBook-Pro.local
        name: Java HotSpot(TM) 64-Bit Server VM
        osArch: x86_64
        osName: Mac OS X
        vendor: Oracle Corporation
        version: 25.131-b11
        Parameters(size -> 300000): 6.500884
        Parameters(size -> 600000): 13.019679
        Parameters(size -> 900000): 22.756615
        Parameters(size -> 1200000): 25.481007
        Parameters(size -> 1500000): 33.129888
    

    与产生的 zipWithIndex 方法相比

        :Benchmark Range.GroupBy::
        cores: 8
        hostname: xxxxx-MacBook-Pro.local
        name: Java HotSpot(TM) 64-Bit Server VM
        osArch: x86_64
        osName: Mac OS X
        vendor: Oracle Corporation
        version: 25.131-b11
        Parameters(size -> 300000): 9.57414
        Parameters(size -> 600000): 18.569085
        Parameters(size -> 900000): 28.233822
        Parameters(size -> 1200000): 36.975254
        Parameters(size -> 1500000): 47.447057
    

    代码:

    implicit class GroupBy[A](val t: TraversableOnce[A]) {
      def sortedGroupBy[K](f: A => K)(implicit ordering: Ordering[K]): immutable.SortedMap[K, ArrayBuffer[A]] = {
        val m = mutable.SortedMap.empty[K, ArrayBuffer[A]]
        for (elem <- t) {
          val key = f(elem)
          val bldr = m.getOrElseUpdate(key, mutable.ArrayBuffer[A]())
          bldr += elem
        }
        val b = immutable.SortedMap.newBuilder[K, ArrayBuffer[A]]
        for ((k, v) <- m) {
          b += ((k, v.result))
        }
        b.result
      }
    }
    

    示例:val sizes = Gen.range("size")(300000, 1500000, 300000)groupByOrdered(_ % 10)

    【讨论】:

    • 纯代码答案通常不是很有帮助。对于这么老的问题,并且已经有 3 个广受好评的答案,您应该描述您的答案与已经提交的答案有何不同。
    • 我明白你的意思了!
    猜你喜欢
    • 2012-03-08
    • 2011-09-06
    • 2010-10-14
    • 2012-12-12
    • 2011-04-07
    • 2011-01-23
    • 1970-01-01
    • 1970-01-01
    • 2018-12-10
    相关资源
    最近更新 更多