【问题标题】:What's Scala's idiomatic way to split a List by separator?Scala 通过分隔符拆分列表的惯用方式是什么?
【发布时间】:2020-12-08 00:35:31
【问题描述】:

如果我有一个字符串类型的列表,

scala> val items = List("Apple","Banana","Orange","Tomato","Grapes","BREAK","Salt","Pepper","BREAK","Fish","Chicken","Beef")
items: List[java.lang.String] = List(Apple, Banana, Orange, Tomato, Grapes, BREAK, Salt, Pepper, BREAK, Fish, Chicken, Beef)

如何根据特定的字符串/模式("BREAK",在这种情况下)将其拆分为 n 单独的列表。

我曾考虑用indexOf 找到"BREAK" 的位置,然后以这种方式拆分列表,或者使用与takeWhile (i => i != "BREAK") 类似的方法,但我想知道是否有更好的方法?

如果有帮助,我知道 items 列表中只会有 3 组项目(因此有 2 个 "BREAK" 标记)。

【问题讨论】:

  • splitAt 可能更合适,但烦人的是BREAK 仍然存在,需要手动删除。
  • 这个问题类似,虽然不完全相同:stackoverflow.com/q/7293617/770361
  • +1 给 Luigi 以从 Martin Odersky 本人那里找到答案 :)

标签: java list scala


【解决方案1】:
def splitBySeparator[T](l: List[T], sep: T): List[List[T]] = {
  l.span( _ != sep ) match {
    case (hd, _ :: tl) => hd :: splitBySeparator(tl, sep)
    case (hd, _) => List(hd)
  }
}
val items = List("Apple","Banana","Orange","Tomato","Grapes","BREAK","Salt","Pepper","BREAK","Fish","Chicken","Beef")
splitBySeparator(items, "BREAK")

结果:

res1: List[List[String]] = List(List(Apple, Banana, Orange, Tomato, Grapes), List(Salt, Pepper), List(Fish, Chicken, Beef))

更新: 上述版本虽然简洁有效,但有两个问题:它不能很好地处理边缘情况(如List("BREAK")List("BREAK", "Apple", "BREAK"),并且不是尾递归。所以这是解决此问题的另一个(必要)版本:

import collection.mutable.ListBuffer
def splitBySeparator[T](l: Seq[T], sep: T): Seq[Seq[T]] = {
  val b = ListBuffer(ListBuffer[T]())
  l foreach { e =>
    if ( e == sep ) {
      if  ( !b.last.isEmpty ) b += ListBuffer[T]()
    }
    else b.last += e
  }
  b.map(_.toSeq)
}

它在内部使用ListBuffer,很像我在splitBySeparator 的第一个版本中使用的List.span 的实现。

【讨论】:

  • 这不是尾递归,所以在长列表中谨慎使用
  • 炖肉,好点——我的名单会相对较短,所以我不必担心。
  • @stew:确实。我用尾递归的版本更新了答案(以及对边缘情况的击球手处理)。
  • 我确实遇到了List("BREAK") 边缘情况,所以谢谢你的修改
【解决方案2】:

另一种选择:

val l = Seq(1, 2, 3, 4, 5, 9, 1, 2, 3, 4, 5, 9, 1, 2, 3, 4, 5, 9, 1, 2, 3, 4, 5)

l.foldLeft(Seq(Seq.empty[Int])) {
  (acc, i) =>
    if (i == 9) acc :+ Seq.empty
    else acc.init :+ (acc.last :+ i)
}

// produces:
List(List(1, 2, 3, 4, 5), List(1, 2, 3, 4, 5), List(1, 2, 3, 4, 5), List(1, 2, 3, 4, 5))

【讨论】:

  • List末尾添加元素效率不高
【解决方案3】:

这个怎么样:使用scan 找出列表中的每个元素属于哪个部分。

val l = List("Apple","Banana","Orange","Tomato","Grapes","BREAK","Salt","Pepper","BREAK","Fish","Chicken","Beef")
val count = l.scanLeft(0) { (n, s) => if (s=="BREAK") n+1 else n } drop(1)
val paired = l zip count
(0 to count.last) map { sec => 
  paired flatMap { case (x, c) => if (c==sec && x!="BREAK") Some(x) else None }  
}
// Vector(List(Apple, Banana, Orange, Tomato, Grapes), List(Salt, Pepper), List(Fish, Chicken, Beef))

【讨论】:

    【解决方案4】:

    这也不是尾递归的,但在边缘情况下可以:

    def splitsies[T](l:List[T], sep:T) : List[List[T]] = l match {
      case head :: tail =>
        if (head != sep)
          splitsies(tail,sep) match {
            case h :: t => (head :: h) :: t
            case Nil => List(List(head))
          }
        else
          List() :: splitsies(tail, sep)
      case Nil => List()
    }
    

    唯一烦人的事:

    scala> splitsies(List("BREAK","Tiger"),"BREAK")
    res6: List[List[String]] = List(List(), List(Tiger))
    

    如果您想更好地处理以分隔符开头的情况,请查看 Martin 的 answer 中使用 span 的内容(对于一个稍微不同的问题)。

    【讨论】:

      【解决方案5】:

      使用 List.unfold(Scala 2.13 及更高版本):

      val p: String => Boolean = _ != "BREAK"
      
      val result: List[List[String]] = List.unfold(items) {
        case Nil =>
          None
        case l if p(l.head) =>
          Some(l.span(p))
        case _ :: tail =>
          Some(tail.span(p))
      }
      

      代码在Scastie 运行。

      使用reverse + foldLeft

      def splitAtElement[T](list: List[T], element: T): List[List[T]] = {
        list.reverse.foldLeft(List(List[T]()))((l, currentElement) => {
          if (currentElement == element) {
            List() :: l
          } else {
            (currentElement :: l.head) :: l.tail
          }
        })
      }
      

      代码在Scastie 运行。

      使用foldRight:

      def splitBySeparator[T](list: List[T], sep: T): List[List[T]] = {
        list.foldRight(List(List[T]()))((s, l) => {
          if (sep == s) {
            List() :: l
          } else {
            (s :: l.head) :: l.tail
          }
        }).filter(_.nonEmpty)
      }
      

      代码在Scastie 运行。

      【讨论】:

        【解决方案6】:
        val q = items.mkString(",").split("BREAK").map("(^,|,$)".r.replaceAllIn(_, "")).map(_.split(","))
        

        这里的“,”是一个唯一的分隔符,它不会出现在项目列表的任何字符串中。如果需要,我们可以选择不同的分隔符。

        items.mkString(",") 将所有内容组合成一个字符串

        .split("BREAK") // which we then split using "BREAK" as delimiter to get a list
        
        .map("(^,|,$)".r.replaceAllIn(_, "")) // removes the leading/trailing commas of each element of the list in previous step
        
        .map(_.split(",")) // splits each element using comma as seperator to give a list of lists
        
        
        scala> val q = items.mkString(",").split("BREAK").map("(^,|,$)".r.replaceAllIn(_, "")).map(_.split(","))
        q: Array[Array[String]] = Array(Array(Apple, Banana, Orange, Tomato, Grapes), Array(Salt, Pepper), Array(Fish, Chicken, Beef))
        
        scala> q(0)
        res21: Array[String] = Array(Apple, Banana, Orange, Tomato, Grapes)
        
        scala> q(1)
        res22: Array[String] = Array(Salt, Pepper)
        
        scala> q(2)
        res23: Array[String] = Array(Fish, Chicken, Beef)
        

        【讨论】:

          猜你喜欢
          • 2019-05-11
          • 2018-03-17
          • 1970-01-01
          • 1970-01-01
          • 2021-06-14
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多