【问题标题】:Efficient iteration with index in Scala在 Scala 中使用索引进行高效迭代
【发布时间】:2011-10-13 14:14:23
【问题描述】:

由于 Scala 没有带有索引的旧 Java 样式 for 循环,

// does not work
val xs = Array("first", "second", "third")
for (i=0; i<xs.length; i++) {
  println("String #" + i + " is " + xs(i))
}

我们如何在不使用var 的情况下高效地进行迭代?

你可以这样做

val xs = Array("first", "second", "third")
val indexed = xs zipWithIndex
for (x <- indexed) println("String #" + x._2 + " is " + x._1)

但是列表被遍历了两次——效率不高。

【问题讨论】:

  • 这些都是很好的回应。我从 Java 'for' 循环中缺少的是拥有多个初始化程序的能力,以及使用不仅仅是增量/减量来“迭代”的能力。这是 Java 比 Scala 更简洁的一个例子。
  • ...“迭代”不仅仅使用增量/减量...在 scala 中,可以使用 step 进行迭代,或者在循环头中使用“if”条件进行迭代。或者您正在寻找其他东西?
  • /*Java*/ for(int i=0, j=0; i+j
  • @snappy :在我看来,最自然的 Scala 转换是 while 循环。我记得,几年前曾有过一场争论,Scala 是否应该继承 Java 的 for(;;) 循环,最终决定这样做的好处不足以证明增加的复杂性是合理的。

标签: scala iteration


【解决方案1】:

我有以下方法

object HelloV2 {

   def main(args: Array[String]) {

     //Efficient iteration with index in Scala

     //Approach #1
     var msg = "";

     for (i <- args.indices)
     {
       msg+=(args(i));
     }
     var msg1="";

     //Approach #2
     for (i <- 0 until args.length) 
     {
       msg1 += (args(i));
     }

     //Approach #3
     var msg3=""
     args.foreach{
       arg =>
        msg3 += (arg)
     }


      println("msg= " + msg);

      println("msg1= " + msg1);

      println("msg3= " + msg3);

   }
}

【讨论】:

    【解决方案2】:

    确实,在集合上调用zipWithIndex 将遍历它并为这些对创建一个新集合。为避免这种情况,您可以在集合的迭代器上调用 zipWithIndex。这只会返回一个新的迭代器,在迭代时跟踪索引,因此无需创建额外的集合或额外的遍历。

    这就是scala.collection.Iterator.zipWithIndex 当前在 2.10.3 中的实现方式:

      def zipWithIndex: Iterator[(A, Int)] = new AbstractIterator[(A, Int)] {
        var idx = 0
        def hasNext = self.hasNext
        def next = {
          val ret = (self.next, idx)
          idx += 1
          ret
        }
      }
    

    这甚至应该比在集合上创建视图更有效。

    【讨论】:

      【解决方案3】:

      在 scala 中循环非常简单。 为 ex. 创建您选择的任何数组。

      val myArray = new Array[String](3)
      myArray(0)="0";
      myArray(1)="1";
      myArray(2)="2";
      

      循环的类型,

      for(data <- myArray)println(data)
      
      for (i <- 0 until myArray.size)
      println(i + ": " + myArray(i))
      

      【讨论】:

        【解决方案4】:

        这个怎么样?

        val a = Array("One", "Two", "Three")
        a.foldLeft(0) ((i, x) => {println(i + ": " + x); i + 1;} )
        

        输出:

        0: One
        1: Two
        2: Three
        

        【讨论】:

          【解决方案5】:

          建议的解决方案受到以下事实的影响:它们要么显式迭代集合,要么将集合填充到函数中。坚持使用 Scala 的惯用语并将索引放在通常的 map 或 foreach 方法中更为自然。这可以使用记忆来完成。生成的代码可能看起来像

          myIterable map (doIndexed(someFunction))
          

          这是实现此目的的一种方法。考虑以下实用程序:

          object TraversableUtil {
              class IndexMemoizingFunction[A, B](f: (Int, A) => B) extends Function1[A, B] {
                  private var index = 0
                  override def apply(a: A): B = {
                      val ret = f(index, a)
                      index += 1
                      ret
                  }
              }
          
              def doIndexed[A, B](f: (Int, A) => B): A => B = {
                  new IndexMemoizingFunction(f)
              }
          }
          

          这已经是你所需要的。您可以按如下方式应用它:

          import TraversableUtil._
          List('a','b','c').map(doIndexed((i, char) => char + i))
          

          列表中的结果

          List(97, 99, 101)
          

          这样,您可以使用通常的 Traversable-functions,但要以包装有效函数为代价。享受吧!

          【讨论】:

            【解决方案6】:

            一种简单高效的方式,灵感来自transformSeqLike.scala中的实现

                var i = 0
                xs foreach { el =>
                  println("String #" + i + " is " + xs(i))
                  i += 1
                }
            

            【讨论】:

              【解决方案7】:

              更多迭代方法:

              scala>  xs.foreach (println) 
              first
              second
              third
              

              foreach 和类似的 map,它会返回一些东西(函数的结果,对于 println,单位,因此是一个单位列表)

              scala> val lens = for (x <- xs) yield (x.length) 
              lens: Array[Int] = Array(5, 6, 5)
              

              使用元素,而不是索引

              scala> ("" /: xs) (_ + _) 
              res21: java.lang.String = firstsecondthird
              

              折叠

              for(int i=0, j=0; i+j<100; i+=j*2, j+=i+2) {...}
              

              可以通过递归来完成:

              def ijIter (i: Int = 0, j: Int = 0, carry: Int = 0) : Int =
                if (i + j >= 100) carry else 
                  ijIter (i+2*j, j+i+2, carry / 3 + 2 * i - 4 * j + 10) 
              

              进位部分只是一些例子,用 i 和 j 做一些事情。它不必是一个 Int。

              对于更简单的东西,更接近通常的 for 循环:

              scala> (1 until 4)
              res43: scala.collection.immutable.Range with scala.collection.immutable.Range.ByOne = Range(1, 2, 3)
              
              scala> (0 to 8 by 2)   
              res44: scala.collection.immutable.Range = Range(0, 2, 4, 6, 8)
              
              scala> (26 to 13 by -3)
              res45: scala.collection.immutable.Range = Range(26, 23, 20, 17, 14)
              

              或无序:

              List (1, 3, 2, 5, 9, 7).foreach (print) 
              

              【讨论】:

                【解决方案8】:

                有人提到Scala确实for循环的语法:

                for (i <- 0 until xs.length) ...
                

                或者干脆

                for (i <- xs.indices) ...
                

                但是,您也要求效率。事实证明,Scala for 语法实际上是 mapforeach 等高阶方法的语法糖。因此,在某些情况下,这些循环可能效率低下,例如How to optimize for-comprehensions and loops in Scala?

                (好消息是 Scala 团队正在努力改进这一点。这是错误跟踪器中的问题:https://issues.scala-lang.org/browse/SI-4633

                为了获得最大效率,可以使用while 循环,或者,如果您坚持删除var 的使用,尾递归:

                import scala.annotation.tailrec
                
                @tailrec def printArray(i: Int, xs: Array[String]) {
                  if (i < xs.length) {
                    println("String #" + i + " is " + xs(i))
                    printArray(i+1, xs)
                  }
                }
                printArray(0, Array("first", "second", "third"))
                

                请注意,可选 @tailrec 注释对于确保该方法实际上是尾递归的很有用。 Scala 编译器将尾递归调用转换为等效于 while 循环的字节码。

                【讨论】:

                • +1 用于提及索引方法/函数,因为我发现它更可取,因为它实际上消除了一系列单独的编程错误。
                • 这里需要注意的是,如果xs是任何一种链表(比如广泛使用的List),像xs(i)这样通过索引访问它的元素都是线性的,因此for (i &lt;- xs.indices) println(i + " : " + xs(i)) 的性能甚至比 for((x, i) &lt;- xs.zipWithIndex) println(i + " : " + x) 还要差,因为它导致的不仅仅是两次遍历。因此,@didierd 建议使用视图的答案应该被接受为最普遍和最惯用的一个,IMO。
                • 如果需要最大效率(例如,在数值计算中),索引数组比遍历链表更快。链表的节点是单独堆分配的,跨不同内存位置的跳转不能很好地使用 CPU 缓存。如果使用view,即使是高级别的抽象也会给堆和GC带来更大的压力。根据我的经验,通过避免数字代码中的堆分配,性能通常可以提高 10 倍。
                【解决方案9】:

                另一种方式:

                scala> val xs = Array("first", "second", "third")
                xs: Array[java.lang.String] = Array(first, second, third)
                
                scala> for (i <- xs.indices)
                     |   println(i + ": " + xs(i))
                0: first
                1: second
                2: third
                

                【讨论】:

                • 我真的很喜欢您指出索引方法/功能。它降低了复杂性并几乎消除了一整套“逐一”错误,这是所有软件工程中最常见的编程错误/错误。
                【解决方案10】:

                stdlib 中没有任何东西可以在不创建元组垃圾的情况下为您做这件事,但编写您自己的并不难。不幸的是,我从来没有费心弄清楚如何做正确的 CanBuildFrom 隐式雨舞来使这些东西在它们所应用的集合类型中通用,但如果可能的话,我相信有人会启发我们。 :)

                def foreachWithIndex[A](as: Traversable[A])(f: (Int,A) => Unit) {
                  var i = 0
                  for (a <- as) {
                    f(i, a)
                    i += 1
                  }
                }
                
                def mapWithIndex[A,B](in: List[A])(f: (Int,A) => B): List[B] = {
                  def mapWithIndex0(in: List[A], gotSoFar: List[B], i: Int): List[B] = {
                    in match {
                      case Nil         => gotSoFar.reverse
                      case one :: more => mapWithIndex0(more, f(i, one) :: gotSoFar, i+1)
                    }
                  }
                  mapWithIndex0(in, Nil, 0)
                }
                
                // Tests....
                
                @Test
                def testForeachWithIndex() {
                  var out = List[Int]()
                  ScalaUtils.foreachWithIndex(List(1,2,3,4)) { (i, num) =>
                    out :+= i * num
                  }
                  assertEquals(List(0,2,6,12),out)
                }
                
                @Test
                def testMapWithIndex() {
                  val out = ScalaUtils.mapWithIndex(List(4,3,2,1)) { (i, num) =>
                    i * num
                  }
                
                  assertEquals(List(0,3,4,3),out)
                }
                

                【讨论】:

                • 添加到标准库中肯定会有意义。
                • 我不太确定,因为如果你想遵循通常的 foreach/map API,你无论如何都会被元组所困。
                【解决方案11】:

                实际上,scala 有带有索引的旧 Java 风格循环:

                scala> val xs = Array("first","second","third")
                xs: Array[java.lang.String] = Array(first, second, third)
                
                scala> for (i <- 0 until xs.length)
                     | println("String # " + i + " is "+ xs(i))
                
                String # 0 is first
                String # 1 is second
                String # 2 is third
                

                其中0 until xs.length0.until(xs.length) 是一个RichInt 方法,它返回适合循环使用的Range

                另外,您可以尝试使用to 循环:

                scala> for (i <- 0 to xs.length-1)
                     | println("String # " + i + " is "+ xs(i))
                String # 0 is first
                String # 1 is second
                String # 2 is third
                

                【讨论】:

                • xs(i) 在列表中将复杂度提高到 O(n^2)
                • @Vadzim 是的,但在 Java 中也是如此,因为您在带有 LinkedList 的索引上使用了 for 循环
                • 对于数组上的xs(i),上面的代码是O(n),对吧?由于 Scala 中的数组提供近乎恒定的时间随机访问?
                • @dhfromkorea 是的,对于数组来说应该很快(实际上是 O(n))
                【解决方案12】:

                比遍历两次更糟糕,它创建了一个中间数组对。 您可以使用view。当您执行collection.view 时,您可以将后续调用视为在迭代期间懒惰地执行。如果你想取回一个适当的完全实现的集合,你最后打电话给force。在这里,这将是无用且昂贵的。所以把你的代码改成

                for((x,i) <- xs.view.zipWithIndex) println("String #" + i + " is " + x)
                

                【讨论】:

                • 好主意,只有一次遍历,但它也会创建 n 对,即使它没有正确创建新集合。
                • 完全正确。好吧,可能有一个模糊的希望,即 JVM 可能会优化这些创建,但我不会指望这一点。我没有看到一个不基于迭代索引的解决方案。
                • @snappy 这个应该被选为答案!在大多数其他答案中都建议按索引访问元素,这违反了 Scala 的功能特性,并且在链表(如 List,Scala 中最常用的集合)上表现得非常糟糕——而不仅仅是在它们上。查看apply 操作over here。在类似链表的集合中,每次按索引访问元素都会导致遍历列表。
                • 这里展示了完全不同的方法:stackoverflow.com/questions/6821194/…
                • 为什么这样高效?它正在创建一个新的数组对象,并使用了一个额外的函数(`view'),所以我很难理解为什么这对开发人员和机器都很有效,除了感觉非常地道。
                猜你喜欢
                • 2012-01-23
                • 2013-02-14
                • 2015-12-07
                • 1970-01-01
                • 2018-04-23
                • 1970-01-01
                • 2012-04-29
                • 1970-01-01
                • 2018-07-01
                相关资源
                最近更新 更多