【问题标题】:groupBy on List as LinkedHashMap instead of Map列表上的 groupBy 作为 LinkedHashMap 而不是 Map
【发布时间】:2019-08-22 14:36:06
【问题描述】:

我正在使用 Scala 处理 XML,并将 XML 转换为我自己的数据结构。目前,我使用普通的 Map 实例来保存(子)元素,但是,XML 中元素的顺序会以这种方式丢失,并且我无法重现原始 XML。

因此,我想使用LinkedHashMap 实例而不是Map,但是我在节点列表上使用groupBy,这会创建一个Map

例如:

  def parse(n:Node): Unit = 
  {
    val leaves:Map[String, Seq[XmlItem]] =
      n.child
        .filter(node => { ... })
        .groupBy(_.label)
        .map((tuple:Tuple2[String, Seq[Node]]) =>
        {
          val items = tuple._2.map(node =>
          {
            val attributes = ...

            if (node.text.nonEmpty)
              XmlItem(Some(node.text), attributes)
            else
              XmlItem(None, attributes)
          })

          (tuple._1, items)
        })

      ...
   }

在此示例中,我希望 leaves 的类型为 LinkedHashMap 以保留 n.child 的顺序。我怎样才能做到这一点?

注意:我按标签/标记名分组,因为元素可以出现多次,并且对于每个标签/标记名,我在我的数据结构中保留一个元素列表。


解决方案
正如@jwvh 所回答的那样,我使用foldLeft 作为groupBy 的替代品。另外,我决定使用LinkedHashMap 而不是ListMap

  def parse(n:Node): Unit = 
  {
    val leaves:mutable.LinkedHashMap[String, Seq[XmlItem]] =
      n.child
        .filter(node => { ... })
        .foldLeft(mutable.LinkedHashMap.empty[String, Seq[Node]])((m, sn) =>
        {
          m.update(sn.label, m.getOrElse(sn.label, Seq.empty[Node]) ++ Seq(sn))
          m
        })
        .map((tuple:Tuple2[String, Seq[Node]]) =>
        {
          val items = tuple._2.map(node =>
          {
            val attributes = ...

            if (node.text.nonEmpty)
              XmlItem(Some(node.text), attributes)
            else
              XmlItem(None, attributes)
          })

          (tuple._1, items)
        })

【问题讨论】:

    标签: scala list sorting hashmap


    【解决方案1】:

    要在ListMap 中获得与.groupBy() 大致相同的值,您可以在您的收藏中使用fold。问题是ListMap 保留了元素添加时的顺序,而不是遇到时的顺序。

    import collection.immutable.ListMap
    
    List('a','b','a','c').foldLeft(ListMap.empty[Char,Seq[Char]]){
      case (lm,c) => lm.updated(c, c +: lm.getOrElse(c, Seq()))
    }
    //res0: ListMap[Char,Seq[Char]] = ListMap(b -> Seq(b), a -> Seq(a, a), c -> Seq(c))
    

    要解决此问题,您可以foldRight 而不是foldLeft。结果是遇到的元素的原始顺序(从左到右扫描)但反向

    List('a','b','a','c').foldRight(ListMap.empty[Char,Seq[Char]]){
      case (c,lm) => lm.updated(c, c +: lm.getOrElse(c, Seq()))
    }
    //res1: ListMap[Char,Seq[Char]] = ListMap(c -> Seq(c), b -> Seq(b), a -> Seq(a, a))
    

    这不一定是件坏事,因为 lastinit ops, O(1) 比 headtail ops, O(n) 更有效)。

    要以原始从左到右的顺序处理ListMap,您可以.toList.reverse 它。

    List('a','b','a','c').foldRight(ListMap.empty[Char,Seq[Char]]){
      case (c,lm) => lm.updated(c, c +: lm.getOrElse(c, Seq()))
    }.toList.reverse
    //res2: List[(Char, Seq[Char])] = List((a,Seq(a, a)), (b,Seq(b)), (c,Seq(c)))
    

    【讨论】:

    • 谢谢,我实验后也想出了foldLeft。另外,我切换到LinkedHashMap。我的代码现在完美地保留了元素顺序,并且可以完美地重新创建原始 XML。我不必切换到foldRight,但正如你所说,它适用于我的foldLeft
    【解决方案2】:

    纯粹不可变的解决方案会很慢。所以我会选择

    import collection.mutable.{ArrayBuffer, LinkedHashMap}
    
    implicit class ExtraTraversableOps[A](seq: collection.TraversableOnce[A]) {
      def orderedGroupBy[B](f: A => B): collection.Map[B, collection.Seq[A]] = {
        val map = LinkedHashMap.empty[B, ArrayBuffer[A]]
    
        for (x <- seq) {
          val key = f(x)
          map.getOrElseUpdate(key, ArrayBuffer.empty) += x
        }
    
        map
    }
    

    要使用,只需将代码中的.groupBy 更改为.orderedGroupBy

    返回的Map 不能使用这种类型进行变异(尽管它可以转换为mutable.Mapmutable.LinkedHashMap),所以它对于大多数用途来说足够安全(而且你如果真的需要,可以在最后创建一个ListMap)。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-12-26
      • 1970-01-01
      • 2023-03-03
      • 1970-01-01
      • 2019-12-20
      • 2019-12-01
      • 1970-01-01
      • 2017-04-29
      相关资源
      最近更新 更多