寻找一种分裂阵列的好方法

时间:2013-01-11 12:40:32

标签: arrays string scala split

我一直在寻找类似于Scala Array String.split 的方法,但我找不到它。

我想要做的是用分隔符分割数组。

例如,分隔以下数组:

val array = Array('a', 'b', '\n', 'c', 'd', 'e', '\n', 'g', '\n')

使用'\n'分隔符,结果应为:

List(Array(a, b), Array(c, d, e), Array(g))

我知道我可以将Array转换为String,并在那里应用split:

array.mkString.split('\n').map(_.toArray)

但我更愿意跳过转换。

到目前为止我的解决方案是递归地使用 span 并且有点过于样板:

  def splitArray[T](array: Array[T], separator: T): List[Array[T]] = {
    def spanRec(array: Array[T], aggResult: List[Array[T]]): List[Array[T]] = {
      val (firstElement, restOfArray) = array.span(_ != separator)
      if (firstElement.isEmpty) aggResult
      else spanRec(restOfArray.dropWhile(_ == separator), firstElement :: aggResult)
    }
    spanRec(array, List()).reverse
  }

我确信Scala中肯定有一些东西我不见了。有什么想法吗?

感谢, 鲁

11 个答案:

答案 0 :(得分:2)

这不是最简洁的实现,但它应该公平地执行并保留数组类型而不需要求助于反射。循环当然可以用递归代替。

由于你的问题没有明确说明我应该对分隔符做什么,我认为它们不应该在输出列表中引起任何条目(参见下面的测试用例)。

def splitArray[T](xs: Array[T], sep: T): List[Array[T]] = {
  var (res, i) = (List[Array[T]](), 0)

  while (i < xs.length) {    
    var j = xs.indexOf(sep, i)
    if (j == -1) j = xs.length
    if (j != i) res ::= xs.slice(i, j)
    i = j + 1
  }

  res.reverse
}

一些测试:

val res1 =
  // Notice the two consecutive '\n'
  splitArray(Array('a', 'b', '\n', 'c', 'd', 'e', '\n', '\n', 'g', '\n'), '\n')

println(res1)
  // List([C@12189646, [C@c31d6f2, [C@1c16b01f)
res1.foreach(ar => {ar foreach print; print(" ")})
  // ab cde g


// No separator
val res2 = splitArray(Array('a', 'b'), '\n')
println(res2)
  // List([C@3a2128d0)
res2.foreach(ar => {ar foreach print; print(" ")})
  // ab


// Only separators
val res3 = splitArray(Array('\n', '\n'), '\n')
println(res3)
  // List()

答案 1 :(得分:1)

您可以使用span方法将数组拆分为两部分,然后在第二部分递归调用split方法。

import scala.reflect.ClassTag

def split[A](l:Array[A], a:A)(implicit act:ClassTag[Array[A]]):Array[Array[A]] = {
  val (p,s) = l.span(a !=)
  p +:  (if (s.isEmpty) Array[Array[A]]() else split(s.tail,a))
}

这不是很有效,因为它具有二次性能。如果你想要快速的东西,一个简单的尾递归解决方案可能是最好的方法。

使用列表而不是数组,您将获得线性性能,并且不需要反射。

答案 2 :(得分:1)

借用了sschaef解决方案的论据:

def split[T](array : Array[T])(where : T=>Boolean) : List[Array[T]] = {
    if (array.isEmpty) Nil
    else {
        val (head, tail) = array span {!where(_)}
        head :: split(tail drop 1)(where)
    }
}                                         //> split: [T](array: Array[T])(where: T => Boolean)List[Array[T]]


val array = Array('a', 'b', '\n', 'c', 'd', 'e', '\n', 'g', '\n')

split(array){_ =='\n'}                    //> res2: List[Array[Char]] = List(Array(a, b), Array(c, d, e), Array(g))

def splitByNewLines(array : Array[Char]) = split(array){_ =='\n'}
splitByNewLines(array)                    //> res3: List[Array[Char]] = List(Array(a, b), Array(c, d, e), Array(g))

答案 3 :(得分:1)

这是一个应该完成这项工作的简短表述:

def split(array:Array[Char], sep:Char) : Array[Array[Char]] = { 
  /* iterate the list from right to left and recursively calculate a 
     pair (chars,list), where chars contains the elements encountered
     since the last occurrence of sep.
  */
  val (chars, list) = array.foldRight[(List[Char],List[Array[Char]])]((Nil,Nil))((x,y) => if (x == sep) (Nil, (y._1.toArray)::y._2) else (x::y._1, y._2)  ); 

  /* if the last element was sep, do nothing; 
     otherwise prepend the last collected chars
  */
  if (chars.isEmpty) 
    list.toArray 
  else 
    (chars.toArray::list).toArray 

}

/* example:
scala> split(array,'\n')
res26: Array[Array[Char]] = Array(Array(a, b), Array(c, d, e), Array(g), Array())
*/

如果我们使用List而不是Array,我们可以稍微概括一下代码:

def split[T](array:List[T], char:T) : List[List[T]] = {
  val (chars, list) = array.foldRight[(List[T],List[List[T]])]((Nil,Nil))((x,y) => if (x == char) (Nil, (y._1)::y._2) else (x::y._1, y._2)  )
  if (chars.isEmpty) list else (chars::list) 
}

/* example:
scala> split(array.toList, '\n')
res32: List[List[Char]] = List(List(a, b), List(c, d, e), List(g), List())

scala> split(((1 to 5) ++ (1 to 5)).toList, 3)
res35: List[List[Int]] = List(List(1, 2), List(4, 5, 1, 2), List(4, 5))
*/

如果这个解决方案被认为是优雅或不可读的,请留给读者和她对函数式编程的偏好:)

答案 4 :(得分:1)

使用foldLeft

进行此操作的简单方法
val f =  array.foldLeft((Array[Char](),List[Array[Char]]()))(
  (acc, char: Char) => {
    char match {
      case '\n' => (Array(),acc._1 :: acc._2)
      case _ => (acc._1 :+ char,acc._2)
    }
  }
)._2.reverse

答案 5 :(得分:1)

我想出了一个针对以下方面的解决方案:

  • 是通用的:您应该能够像Array一样拆分Vector,并且像任意对象的集合一样拆分Char的集合
  • 保留输入的类型:Array[A]被分成Array[Array[A]]Vector[A]被分成Vector[Vector[A]]
  • 允许在需要时使用懒惰方法(通过Iterator
  • 为大多数情况提供一个紧凑的接口(只需在您的集合上调用split方法)

在进行解释之前,请注意,您可以使用here on Scastie之后的代码。

第一步是实现一个Iterator,用于分块您的收藏集:

import scala.language.higherKinds
import scala.collection.generic.CanBuildFrom

final class Split[A, CC[_]](delimiter: A => Boolean, as: CC[A])(
    implicit view: CC[A] => Seq[A], cbf: CanBuildFrom[Nothing, A, CC[A]])
    extends Iterator[CC[A]] {

  private[this] var it: Iterator[A] = view(as).iterator

  private def skipDelimiters() = {
    it = it.dropWhile(delimiter)
  }

  skipDelimiters()

  override def hasNext: Boolean = it.hasNext

  override def next(): CC[A] = {
    val builder = cbf()
    builder ++= it.takeWhile(!delimiter(_))
    skipDelimiters()
    builder.result()
  }

}

我使用谓词而不是值来更灵活地拆分集合,尤其是在拆分非标量值的集合(例如Char s)时。

我正在使用集合类型的隐式视图,以便将其应用于可以视为Seq的所有类型的集合(例如VectorArray s)和CanBuildFrom,以便能够构建我作为输入接收的确切集合类型。

Iterator的实现只是确保删除定界符,并对其余部分进行分块。

我们现在可以使用implicit class提供一个友好的界面,并将split方法添加到所有集合中,两者都允许将谓词或值定义为定界符:

final implicit class Splittable[A, CC[_]](val as: CC[A])(implicit ev1: CC[A] => Seq[A], ev2: CanBuildFrom[Nothing, A, CC[A]], ev3: CanBuildFrom[Nothing, CC[A], CC[CC[A]]]) {

  def split(delimiter: A => Boolean): CC[CC[A]] = new Split(as)(delimiter).to[CC]

  def split(delimiter: A): CC[CC[A]] = new Split(as)(_ == delimiter).to[CC]

}

现在您可以在Char个集合中自由使用您的方法

val a = Array('a', 'b', '\n', 'c', 'd', 'e', '\n', 'g', '\n')
val b = List('\n', '\n', '\n')
val c = Vector('\n', 'c', 'd', 'e', '\n', 'g', '\n')
val d = Array('a', 'b', 'c', 'd', 'e', 'g', '\n')
val e = Array('a', 'b', 'c', 'd', 'e', 'g', '\n')

a.split('\n')
b.split('\n')
c.split('\n')
d.split('\n')
e.split('\n')

和任意对象都一样:

final case class N(n: Int, isDelimiter: Boolean)

Vector(N(1, false), N(2, false), N(3, true), N(4, false), N(5, false)).split(_.isDelimiter)

请注意,通过直接使用迭代器,您可以使用惰性方法,因为您可以看到是否向next方法中添加了调试打印并尝试执行以下操作:

new Split(Vector('\n', 'c', 'd', 'e', '\n', 'g', '\n'))(_ == '\n'}).take(1).foreach(println)

如果需要,可以在Splittable上添加几个返回Iterator的方法,以便可以直接通过它公开惰性方法。

答案 6 :(得分:0)

我不知道任何内置方法,但我提出了一个比你更简单的方法:

def splitOn[A](xs: List[A])(p: A => Boolean): List[List[A]] = xs match {
  case Nil => Nil
  case x :: xs =>
    val (ys, zs) = xs span (!p(_))
    (x :: ys) :: splitOn(zs.tail)(p)
}

// for Array
def splitOn[A : reflect.ClassTag](xs: Array[A])(p: A => Boolean): List[Array[A]] =
  if (xs.isEmpty) List()
  else {
    val (ys, zs) = xs.tail span (!p(_))
    (xs.head +: ys) :: splitOn(zs.tail)(p)
  }

scala> val xs = List('a', 'b', '\n', 'c', 'd', 'e', '\n', 'g', '\n')
xs: List[Char] = 
List(a, b, 
, c, d, e, 
, g, 
)

scala> splitOn(xs)(_ == '\n')
res7: List[List[Char]] = List(List(a, b), List(c, d, e), List(g))

答案 7 :(得分:0)

这个怎么样?没有反射,也没有递归,但尝试使用尽可能多的scala库。

def split[T](a: Array[T], sep: T)(implicit m:ClassManifest[T]): Array[Array[T]] = {
  val is = a.indices filter (a(_) == sep)
  (0 +: (is map (1+))) zip (is :+ (a.size+1)) map { 
    case(from,till) => a.slice(from, till)
  } 
}

可能很慢,但只是为了好玩。 :-)

indices filter为您提供找到分隔符的位置的索引(is)。 在您的示例中,那是2,6,8。我认为这是O(n)

下一行将其转换为(0,2), (3,6), (7,8), (9, 10)。因此k分隔符会产生k+1个范围。 这些将交给slice,其余的工作完成。转换也是O(n),其中n是找到的分隔符数。 (这意味着Array[Char]()的输入将产生Array(Array())而不是更直观的Array(),但这并不太有趣。)

数组追加/前置(:++:)使用数组是浪费的,但是使用允许O(1)追加/的适当集合无法解决任何问题预规划。

答案 8 :(得分:0)

您也可以使用fold:

完成此操作
def splitArray[T](array: Array[T], separator: T) = 
    array.foldRight(List(List.empty[T])) { (c, list) => 
        if (c == separator) Nil :: list 
        else (c :: list.head) :: list.tail
    }.filter(!_.isEmpty).map(_.reverse).toArray

已经被lambda.xy.x提到了,但由于某种原因,它的可读性稍差一点;)

答案 9 :(得分:0)

Pimped版本的通用序列/数组拆分 -

  implicit def toDivide[A, B <% TraversableLike[A, B]](a : B) = new {
    private def divide(x : B, condition: (A) => Boolean) : Iterable[B] = {

      if (x.size > 0)
        x.span(condition) match {
          case (e, f) => if (e.size > 0) Iterable(e) ++ divide(f.drop(1),condition) else Iterable(f)
        }
      else
        Iterable()
    }
    def divide(condition: (A) => Boolean): Iterable[B] = divide(a, condition)
  }

答案 10 :(得分:0)

几乎是单线的:

sed -E 's/REGEX/REPLACE/g' FILENAME

对于给定的val it = array.iterator List.range(0, array.count(_ == '\n')).map(_ => it.takeWhile(_ != '\n').toArray) ,它使用array的{​​{1}}版本,以便调用Iterator的次数与出现分隔符的次数相同。


使用Array(虽然它是一个范围内的映射)的另一版本,但可读性较差,但稍短一些:

.takeWhile

可以通过以下方式将List.tabulate设置为val it = array.iterator List.tabulate(array.count(_ == '\n'))(_ => it.takeWhile(_ != '\n').toArray)

array.mkString.split("\n", -1).map(_.toArray)

并以此方式使用:

Array

要摆脱连续出现两次分隔符而产生的空子数组,可以用implicit class ArrayExtensions[T: ClassTag](array: Array[T]) { def split(sep: T): List[Array[T]] = { val it = array.iterator List.range(0, array.count(_ == sep)).map(_ => it.takeWhile(_ != sep).toArray) } } 用管道传递结果。