Scala - 遍历两个数组

5

Scala的方法是如何迭代两个相同大小的数组,每次迭代访问相同的索引?

      for ((aListItem, bListItem) <- (aList, bList)) {
         // do something with items
      }

将Java的方式应用于Scala:

     for(i <- 0 until aList.length ) {
          aList(i)
          bList(i)
      }

假设这两个列表大小相同。

你现在遇到了什么问题?如果问题是代码无法编译,只需要给元组添加定义即可:'for ((aListItem:Int, bListItem:Int)...' - Ed Staub
1
相关链接:https://dev59.com/lmQm5IYBdhLWcg3w-S6Y - senia
Scala Way(TM)不使用数组(可变的),而是使用列表,但这些对于随机访问(即按索引)效率低下。无论你使用哪种方式,正确的解决方案都在Rex Kerr的答案中:(aList, bList).zipped.foreach{ (a,b) => ??? }。是的,它很高效,因为它不需要为每个元素创建元组(与zip不同)。 - Luigi Plinge
我更新了答案,包括时间信息。@LuigiPlinge - 如果您的数组不是原始类型,则压缩是快速而简单的方法。如果它们是原始类型,装箱开销会让您失望。 - Rex Kerr
好观点,谢谢@Rex。这仅适用于数组,而不适用于列表或向量,因为在后者中,原语已经被装箱了,对吧? - Luigi Plinge
4个回答

17

简而言之: 在速度和便利性之间需要做出权衡;你需要了解你的使用情况来进行适当的选择。


如果您知道两个数组具有相同的长度,并且不需要担心它的速度,最简单和最常规的方法是在for循环中使用zip

for ((a,b) <- aList zip bList) { ??? }

zip 方法创建了一个新的单一数组。但是,为了避免这种开销,您可以在元组上使用 zipped,它会将元素成对呈现给像 foreachmap 这样的方法:

(aList, bList).zipped.foreach{ (a,b) => ??? }
更快的做法是索引到数组中,特别是如果数组包含像Int这样的基本类型,因为上面的通用代码必须将它们装箱。有一个方便的方法indices可以使用:
for (i <- aList.indices) { ??? }

最后,如果您需要尽可能快地进行操作,您可以退回到手动 while 循环或递归,如下所示:

// While loop
var i = 0
while (i < aList.length) {
  ???
  i += 1
}

// Recursion
def loop(i: Int) {
  if (i < aList.length) {
    ???
    loop(i+1)
  }
}
loop(0)

如果您正在计算某个值,而不希望它产生副作用,使用递归时将其传递通常会更快:

// Recursion with explicit result
def loop(i: Int, acc: Int = 0): Int =
  if (i < aList.length) {
    val nextAcc = ???
    loop(i+1, nextAcc)
  }
  else acc

由于您可以随时删除方法定义,因此可以无限制地使用递归。您可以添加@annotation.tailrec注释以确保它可以编译为带有跳转的快速循环,而不是实际占用堆栈空间的递归。

对于计算长度为1024的向量点积的所有不同方法,我们可以将它们与Java中的参考实现进行比较:

public class DotProd {
  public static int dot(int[] a, int[] b) {
    int s = 0;
    for (int i = 0; i < a.length; i++) s += a[i]*b[i];
    return s;
  }
}

另外还有一种等价版本,我们计算字符串长度的点积(这样我们就可以评估对象与基本类型)

normalized time
-----------------
primitive  object  method
---------  ------  ---------------------------------
 100%       100%   Java indexed for loop (reference)
 100%       100%   Scala while loop
 100%       100%   Scala recursion (either way)
 185%       135%   Scala for comprehension on indices
2100%       130%   Scala zipped
3700%       800%   Scala zip

当然,如果你使用原始数据类型,这种情况尤其糟糕!(如果你在 Java 中尝试使用包含 Integer 的 ArrayList 而不是 int 的 Array,时间消耗也会出现类似的巨大跳跃。)请注意,如果存储了对象,则使用“zipped”相当合理。

但要注意避免过早优化!函数式形式(如 zip)具有清晰性和安全性的优点。如果你总是编写 while 循环,因为你认为 "每一点帮助",那么你可能会犯一个错误,因为编写和调试它需要更多时间,而你可以利用那些时间优化程序的其他重要部分。


但是,假设您的数组长度相同是危险的。你确定吗?你会付出多少努力来确保?也许你不应该做出那个假设?

如果你不需要它很快,只是正确,那么你必须选择如果两个数组的长度不相等要怎么做。

如果你想处理所有元素直到较短的长度,那么仍然使用“zip”:

// The second is just shorthand for the first
(aList zip bList).foreach{ case (a,b) => ??? }
for ((a,b) <- (aList zip bList)) { ??? }

// This avoids an intermediate array
(aList, bList).zipped.foreach{ (a,b) => ??? }

如果您想使用默认值填充较短的值,则应

aList.zipAll(bList, aDefault, bDefault).foreach{ case (a,b) => ??? }
for ((a,b) <- aList.zipAll(bList, aDefault, bDefault)) { ??? }

在任何这些情况下,您可以使用formapyield代替foreach生成集合。

如果您需要索引进行计算,或者确实是数组并且需要快速处理它,您将不得不手动进行计算。填充缺少的元素很麻烦(我将其留给读者作为练习),但基本格式如下:

for (i <- 0 until math.min(aList.length, bList.length)) { ??? }

您可以使用 i 索引到 aListbList

如果您确实需要最大速度,可以再次使用(尾递归)或 while 循环:

val n = math.min(aList.length, bList.length)
var i = 0
while (i < n) {
  ???
  i += 1
}

def loop(i: Int) {
  if (i < aList.length && i < bList.length) {
    ???
    loop(i+1)
  }
}
loop(0)

只是一个小提示,zipped现在已经被弃用了。相反,应该使用lazyZip。像这样:a.lazyZip(b).foreach{(a, b) => ??? } - DataBach

2

像这样的:

for ((aListItem, bListItem) <- (aList zip bList)) {
     // do something with items
}

或者使用map,如下所示:

(aList zip bList).map{ case (alistItem, blistItem) => // do something }

更新:

如果想要在不创建中间变量的情况下进行迭代,可以尝试以下方法:

for (i <- 0 until xs.length) ... //xs(i) & ys(i) to access element

或者简单地说
for (i <- xs.indices) ... 

我认为添加的 zip 程序会对性能造成影响,相比 Java 的方式。 - BAR
需要查看源代码才能确定zip是包装还是构造一个新数组。 - BAR
@BAR 这可能有助于性能考虑,但这取决于用例和编译器优化:https://dev59.com/KnE85IYBdhLWcg3wZyqt - nitishagar
@BAR 它确实会创建一个中间层。 - nitishagar
zip 创建一个新的数组并复制数据。需要一种与通常的方式(通过保持索引)同样快的方法。 - BAR
显示剩余2条评论

1
for {
    i <- 0 until Math.min(list1.size, list2.size)
 } yield list1(i) + list2(i)

或者像检查边界等类似的东西。

它仅检查list1的边界。如果列表的大小实际上未同步,它仍然可能会出错。 - Justin Pihony
这段代码将始终越界,因为你使用了包含的 to - Rex Kerr
我已经确认了需要检查边界的答案,但我为您更新了它。 - Rich Henry

1
我会做这样的事情:


aList.indices foreach { i => 
   val (aListItem, bListItem) = (aList(i), bList(i))
   // do something with items
}

我非常喜欢这个。Scala语法和功能简洁明了。 - BAR
1
如果bListaList短,则会抛出异常。 - Rex Kerr
@RexKerr 两个列表的大小将相同。更新帖子以澄清。 - BAR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接