【问题标题】:How to filter a list by multiple predicates in one pass?如何一次通过多个谓词过滤列表?
【发布时间】:2018-12-06 13:27:44
【问题描述】:

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

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))

【问题讨论】:

    标签: scala collections


    【解决方案1】:

    仍然会多次遍历集合的单行答案:

    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 搜索来到这里的,那么您可能想要别的东西:

    AND-ing 多个谓词:

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

    OR-ing 多个谓词:

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

    【讨论】:

    • 你在xs 上调用了filter 三次,所以这并不是真正的“一次通过”。如果xs 是一个流,这将如何工作?
    • @Tim 我不确定你所说的“一次通过”是什么意思。它应该产生三个单独的列表,每个列表都被不同的谓词过滤,所以,不可避免地,它会调用filter三次,因为@987654332的一次调用@ 最多可以产生一个列表;对流做同样的事情会有什么问题?
    • @Tim 对于不缓存先前生成的值的Iterators,这种方法可能会有点问题,但这不是问题的一部分。
    • @AndreyTyukin,假设xs 有十亿个元素。在这种情况下,仅访问它们 3 次是效率低下的根源。对我来说,“一次性”显然意味着我们只想访问集合中的每个元素一次。而您的代码没有实现这一点。 “一次通过”与“在一行代码中”肯定不同
    • 感谢大家的有益讨论。现在我更好地理解了我想要什么:)
    【解决方案2】:

    我相信 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 次,而我的解决方案只访问一次。

    【讨论】:

    • 它现在确实解决了这个问题,我写了一个过滤器多谓词实现,它应该适用于任何迭代器。
    • @AndreyTyukin,我相信使用来自xs 的构建器比总是创建List 更好,但这可能是一个品味问题。
    • 谢谢。我更喜欢fold 解决方案。
    • @SergGr 是的,当然,也可以抛出一个 CanBuildFrom,以将结果的集合类型与输入的类型联系起来......我仍然更喜欢单线,直到分析器告诉我不这样做。这个答案以某种方式将其他答案的评论与实际答案混为一谈。
    猜你喜欢
    • 2022-11-26
    • 2013-08-09
    • 1970-01-01
    • 1970-01-01
    • 2012-04-10
    • 1970-01-01
    • 1970-01-01
    • 2018-10-16
    • 2019-03-17
    相关资源
    最近更新 更多