如何一次通过多个谓词过滤列表?

时间:2018-12-06 13:27:44

标签: scala collections

假设我通过一些谓词过滤列表,例如

val xs = List(1, 0, -1, 2, 3, 4, 5, -6, 5, 0)
val pred1: Int => Boolean = _ > 0
val pred2: Int => Boolean = _ < 0
val pred3: Int => Boolean = _ % 2 == 0

val xs1 = xs.filter(pred1) // List(1, 2, 3, 4, 5, 5)
val xs2 = xs.filter(pred2) // List(-1, -6)
val xs3 = xs.filter(pred3) // List(0, 2, 4, -6, 0)

如何仅一次通过所有这些谓词过滤列表?

def filterByFew(xs: List[Int], preds: List[Int => Boolean]): List[List[Int]] = ???

filterByFew(xs, List(pred1, pred2, pred3))应该返回
   List(List(1, 2, 3, 4, 5, 5), List(-1, -6), List(0, 2, 4, -6, 0))

2 个答案:

答案 0 :(得分:2)

单线答案仍然遍历集合:

List(pred1, pred2, pred3).map(xs.filter)

作为方法:

def filterByFew(xs: List[Int], preds: List[Int => Boolean]): List[List[Int]] = 
  preds.map(xs.filter)

它与流的工作方式几乎相同:

val p1 = (x: Int) => x % 2 == 0
val p2 = (x: Int) => x % 3 == 0

val preds = List(p1, p2)
val str = Stream.from(0)

val filteredStreams = preds.map(str.filter)
filteredStreams foreach { s => println(s.take(10).toList) }

// Output:
// List(0, 2, 4, 6, 8, 10, 12, 14, 16, 18)
// List(0, 3, 6, 9, 12, 15, 18, 21, 24, 27)

但不要在REPL中尝试:REPL会挂起为什么要尝试显示中间结果的原因。


遍历集合一次

如果您真的负担不起遍历集合的次数,那么我看不到任何有效的解决方法,最简单的方法似乎是重新实现filter,但要使用多个可变的构建器:

def filterByMultiple[A](
  it: Iterator[A],
  preds: List[A => Boolean]
): List[List[A]] = {
  val n = preds.size
  val predsArr = preds.toArray
  val builders = Array.fill(n){
    new collection.mutable.ListBuffer[A]
  }
  for (a <- it) {
    for (j <- 0 until n) {
      if (predsArr(j)(a)) {
        builders(j) += a
      }
    }
  }
  builders.map(_.result)(collection.breakOut)
}


filterByMultiple((0 to 30).iterator, preds) foreach println
// Output:
// List(0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30)
// List(0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30)

如果您来自Google搜索,那么您可能还需要其他东西:

与多个谓词相加:

def filterByAnd(xs: List[Int], preds: List[Int => Boolean]) = 
  xs.filter(x => preds.forall(p => p(x)))

或多个谓词:

def filterByOr(xs: List[Int], preds: List[Int => Boolean]) = 
  xs.filter(x => preds.exists(p => p(x)))

答案 1 :(得分:1)

我相信Andrey Tyukin的回答不会解决问题的“一口气”。如果不必保留元素的顺序,那么我认为以下实现会相当有效:

def filterByFew[A](xs: Traversable[A], preds: List[A => Boolean]): List[List[A]] = {
  xs.foldLeft(List.fill(preds.size)(List.empty[A]))((acc, el) => {
    acc.zip(preds).map({
      case (l, p) => if (p(el)) el :: l else l
    })
  })
}

如果必须保留顺序,则简单的解决方案是在filterByFew的末尾反转所有内部列表,但是如果过滤器的选择性不是很高,它将有效地遍历整个集合多次。另一个解决方案是这样的:

def filterByFew2[A](xs: Traversable[A], preds: List[A => Boolean]): List[Traversable[A]] = {
  val builders = xs.foldLeft(List.fill(preds.size)(xs.companion.newBuilder[A]))((acc, el) => {
    acc.zip(preds).foreach({
      case (b, p) => if (p(el)) b += el
    })
    acc
  })
  builders.map(b => b.result())
}

FP较少,但性能更好。实际上,这类似于在标准库中实现filter的方式。

一个简单的测试来确保它能像声明的那样工作:

def test(): Unit = {
  val xs0 = List(1, 0, -1, 2, 3, 4, 5, -6, 5, 0)
  val xs = xs0.view.map(x => {
    println(s"accessing $x")
    x
  })
  val pred1: Int => Boolean = _ > 0
  val pred2: Int => Boolean = _ < 0
  val pred3: Int => Boolean = _ % 2 == 0
  val preds = List(pred1, pred2, pred3)
  val res = preds.map(xs.filter)
  println(res)
  println("---------------")
  println(filterByFew(xs, preds))
  println("---------------")
  println(filterByFew2(xs, preds))

}

view是一种惰性方法,因此我们将记录对基础集合的每次访问。显而易见,Andrey的代码访问每个元素3次,而我的解决方案只访问一次。