【问题标题】:Scala: Best way to filter & map in one iterationScala:一次迭代中过滤和映射的最佳方法
【发布时间】:2015-08-30 09:58:47
【问题描述】:

我是 Scala 的新手,并试图找出过滤和映射集合的最佳方法。这是一个解释我的问题的玩具示例。

方法 1: 这很糟糕,因为我在列表中迭代了两次,并且每次迭代都计算相同的值。

val N = 5
val nums = 0 until 10
val sqNumsLargerThanN = nums filter { x: Int => (x * x) > N } map { x: Int => (x * x).toString }

方法 2: 这稍微好一些,但我仍然需要计算 (x * x) 两次。

val N = 5
val nums = 0 until 10
val sqNumsLargerThanN = nums collect { case x: Int if (x * x) > N => (x * x).toString }

那么,是否可以在不遍历集合两次的情况下进行计算并避免重复相同的计算?

【问题讨论】:

    标签: scala dictionary collections filter collect


    【解决方案1】:

    可以使用foldRight

    nums.foldRight(List.empty[Int]) {
      case (i, is) =>
        val s = i * i
        if (s > N) s :: is else is
      }
    

    foldLeft 也可以实现类似的目标,但结果列表的顺序会相反(由于 foldLeft 的关联性。

    如果您想使用 Scalaz 也可以选择

    import scalaz.std.list._
    import scalaz.syntax.foldable._
    
    nums.foldMap { i =>
      val s = i * i
      if (s > N) List(s) else List()
    }
    

    【讨论】:

    • 请注意,使用默认的foldRight 如果您的列表长度超过一千个左右,您将溢出堆栈。此外,Scalaz 版本与flatMap 相比没有任何优势。
    【解决方案2】:

    典型的方法是使用iterator(如果可能)或view(如果iterator 不起作用)。这并没有完全避免两次遍历,但确实避免了创建完整大小的中间集合。然后你先map,然后再filter,如果需要,再map

    xs.iterator.map(x => x*x).filter(_ > N).map(_.toString)
    

    这种方法的优点是它真的很容易阅读,而且由于没有中间集合,它的效率相当高。

    如果您因为这是性能瓶颈而问,那么答案通常是编写尾递归函数或使用旧式 while 循环方法。例如,在你的情况下

    def sumSqBigN(xs: Array[Int], N: Int): Array[String] = {
      val ysb = Array.newBuilder[String]
      def inner(start: Int): Array[String] = {
        if (start >= xs.length) ysb.result
        else {
          val sq = xs(start) * xs(start)
          if (sq > N) ysb += sq.toString
          inner(start + 1)
        }
      }
      inner(0)
    }
    

    您也可以在inner 中向前传递参数,而不是使用外部构建器(对求和特别有用)。

    【讨论】:

    • 嗨 Rex - 你的意思是它不能完全避免两次遍历?
    • @sourcedelica - 每个迭代器在遍历列表时,也(必然)遍历之前的迭代器。所以它们都以锁步的方式遍历,但是如果你先映射,然后过滤,再映射,你实际上有嵌套三层的 next/hasNext 调用。
    【解决方案3】:

    您可以使用collect,它将一个偏函数应用于定义它的集合的每个值。你的例子可以改写如下:

    val sqNumsLargerThanN = nums collect {
        case (x: Int) if (x * x) > N => (x * x).toString
    }
    

    【讨论】:

    • 为什么有人反对这个答案? collect 似乎是一种非常惯用的方式。
    • 这不是和我的“方法2”一模一样吗?
    • 是的,它与上面的方法#2 相同,并且按照 collect 的定义,这对我来说似乎完全合理;它准确地说明了它的作用。这并不是说上面阐述的其他方法更好或更差。
    【解决方案4】:

    我还没有确认这是否真的是单程,但是:

      val sqNumsLargerThanN = nums flatMap { x =>
        val square = x * x
        if (square > N) Some(x) else None
      }
    

    【讨论】:

    • 我想问一下,为选项层包装每个元素的加载会比计算 x * x 两次更轻吗? Option 对象创建成本可以忽略不计? (我是 C++ 的 Scala 新手。)
    • 直接回答你的问题,不,期权分配不是免费的。不过它很便宜。多年来,JVM GC 在循环中分配和收集小对象方面做得非常好。所以虽然不是免费的,但这几乎不是我开始优化的地方。
    • 此外,我应该提到,虽然这是一个有趣的难题,但在函数式编程的世界中,尝试最小化集合的传递次数通常不是获得性能的最佳方式。这些东西在 C/C++ 世界中很常见,而在 JVM 中则不太常见。话虽如此,让我们假设您的收藏量很大,例如 8GB。那么你真的只想传递一次,我会坚持收集,或者使用惰性收集。双重乘法将被 JIT 优化掉
    • 这是一次通过,但它有点作弊,因为您创建了第二个集合的价值作为选项。 (甚至还有第三个,因为你不能在没有隐式转换为 Iterable 的情况下 flatMap 它。)所以它通常比 iterator 更重量级,即使在某种意义上它在技术上是单次通过iterators 不是这样。
    • 是的,感觉就像在作弊:)
    【解决方案5】:

    一种非常简单的方法,只进行一次乘法运算。它也很懒,所以它只会在需要时执行代码。

    nums.view.map(x=>x*x).withFilter(x => x> N).map(_.toString)
    

    查看here 以了解filterwithFilter 之间的区别。

    【讨论】:

    • 这很有趣。在您链接到的线程中,有一条评论“我认为您不应该自己使用 withFilter (除了隐含在 for 表达式中)”。有理由不使用withFilter
    • 我只在我想创建一个新集合以在以后使用时使用filter。如果我只想将过滤器作为操作管道的中间步骤,我总是使用withFilter
    【解决方案6】:

    考虑到这个理解,

      for (x <- 0 until 10; v = x*x if v > N) yield v.toString
    

    其中展开到一个flatMap在范围内和一个(懒惰的)withFilter到曾经唯一计算的正方形上,并产生一个带有过滤结果的集合。请注意,需要进行一次迭代和一次平方计算(除了创建范围之外)。

    【讨论】:

      【解决方案7】:

      您可以使用flatMap

      val sqNumsLargerThanN = nums flatMap { x =>
        val square = x * x
        if (square > N) Some(square.toString) else None
      }
      

      或者使用 Scalaz,

      import scalaz.Scalaz._
      
      val sqNumsLargerThanN = nums flatMap { x =>
        val square = x * x
        (square > N).option(square.toString)
      }
      

      解决了如何通过一次迭代执行此操作的问题。这在流式传输数据时很有用,例如使用迭代器。

      但是...如果您想要绝对的最快 实现,这不是它。事实上,我怀疑你会使用一个可变的 ArrayList 和一个 while 循环。但只有在分析之后,你才能确定。无论如何,这是另一个问题。

      【讨论】:

        【解决方案8】:

        使用 for 理解会起作用:

        val sqNumsLargerThanN = for {x <- nums if x*x > N } yield (x*x).toString
        

        另外,我不确定,但我认为 scala 编译器对映射前的过滤器很聪明,如果可能的话只会做 1 次。

        【讨论】:

          【解决方案9】:

          我也是初学者,按如下操作

           for(y<-(num.map(x=>x*x)) if y>5 ) { println(y)}
          

          【讨论】:

            猜你喜欢
            • 2018-03-26
            • 2017-12-22
            • 2018-10-07
            • 1970-01-01
            • 2022-07-06
            • 2016-03-21
            • 1970-01-01
            • 2011-05-19
            • 1970-01-01
            相关资源
            最近更新 更多