【问题标题】:Why would iterating over a List be faster than indexing through it?为什么迭代 List 比通过它索引更快?
【发布时间】:2012-05-16 05:19:54
【问题描述】:

阅读Java documentation for the ADT List 上面写着:

List 接口为列表元素的位置(索引)访问提供了四种方法。列表(如 Java 数组)是从零开始的。请注意,对于某些实现(例如 LinkedList 类),这些操作的执行时间可能与索引值成正比。因此,如果调用者不知道实现,则迭代列表中的元素通常比通过它索引更可取。

这到底是什么意思?我不明白得出的结论。

【问题讨论】:

标签: java list iterator


【解决方案1】:

在链表中,每个元素都有一个指向下一个元素的指针:

head -> item1 -> item2 -> item3 -> etc.

要访问item3,可以清楚的看到需要从头部遍历每个节点,直到到达item3,因为不能直接跳转。

因此,如果我想打印每个元素的值,如果我这样写:

for(int i = 0; i < 4; i++) {
    System.out.println(list.get(i));
}

发生了什么:

head -> print head
head -> item1 -> print item1
head -> item1 -> item2 -> print item2
head -> item1 -> item2 -> item3 print item3

效率极低,因为每次您编制索引时,它都会从列表的开头重新开始并遍历每个项目。这意味着您的复杂性实际上是O(N^2) 只是为了遍历列表!

如果我这样做:

for(String s: list) {
    System.out.println(s);
}

然后发生的事情是这样的:

head -> print head -> item1 -> print item1 -> item2 -> print item2 etc.

全部在一次遍历中,即O(N)

现在,转到List 的另一个实现,即ArrayList,它由一个简单的数组支持。在这种情况下,上述两种遍历都是等价的,因为数组是连续的,所以它允许随机跳转到任意位置。

【讨论】:

  • 次要注意:如果索引在列表的后半部分,LinkedList 将从列表末尾开始搜索,但这并没有真正改变根本的低效率。它只是稍微减少了问题。
  • 这是非常低效的。对于较大的 LinkedList - 是的,对于较小的它可以更快地工作 REVERSE_THRESHOLDjava.util.Collections 中设置为 18,看到如此高票的答案而没有评论是很奇怪的。
  • @DanDiplo,如果结构是链接的,是的,它成立。然而,使用 LinkedS 结构是一个小谜。它们几乎总是比数组支持的性能差得多(额外的内存占用、gc 不友好和可怕的局部性)。 C# 中的标准列表是数组支持的。
  • 次要注意:如果您想检查应该使用哪种迭代类型(indexed vs Iterator/foreach),您可以随时测试 List 是否实现 RandomAccess(标记接口):List l = unknownList(); if (l instanceof RandomAccess) /* do indexed loop */ else /* use iterator/foreach */
  • @KK_07k11A0585:实际上,第一个示例中的增强 for 循环被编译为与第二个示例一样的迭代器,因此它们是等价的。
【解决方案2】:

迭代一个带有偏移量的列表进行查找,例如i,类似于Shlemiel thepainter's algorithm

Shlemiel 找到了一份街头画家的工作,负责画虚线 在路中间。第一天他拿了一罐油漆 走到路边,完成 300 码的路。 “真漂亮 好!”他的老板说,“你是一个快速的工人!”然后付给他一个戈比。

第二天,Shlemiel 只完成了 150 码。 “嗯,那不是 几乎和昨天一样好,但你仍然是一个快速的工人。 150码 是可敬的,”并付给他一个戈比。

第二天,Shlemiel 粉刷了 30 码的道路。 “只有三十个!”喊叫 他的老板。 “那不行!第一天你做了十次 这么多工作!怎么回事?”

“我情不自禁,”Shlemiel 说。 “我一天天越走越远 远离油漆罐!”

Source.

这个小故事可能会让你更容易理解内部发生的事情以及它为什么如此低效。

【讨论】:

    【解决方案3】:

    虽然公认的答案肯定是正确的,但我可以指出一个小缺陷。引用都铎:

    现在,转到 List 的另一个实现,即 ArrayList, 那一个由一个简单的数组支持。 在这种情况下,以上两种情况 遍历是等价的,因为数组是连续的,所以它允许 随机跳转到任意位置。

    这并不完全正确。事实是,

    使用 ArrayList,手写的计数循环大约快 3 倍

    source: Designing for Performance, Google's Android doc

    请注意,手写循环指的是索引迭代。我怀疑它是因为与增强的 for 循环一起使用的迭代器。它在由连续数组支持的结构中产生较小的惩罚性能。我也怀疑这可能适用于 Vector 类。

    我的规则是,尽可能使用增强的 for 循环,如果您真的关心性能,请仅对 ArrayLists 或 Vectors 使用索引迭代。在大多数情况下,您甚至可以忽略这一点——编译器可能会在后台对此进行优化。

    我只是想指出,在 Android 开发的上下文中,ArrayList 的两种遍历不一定等价。值得深思。

    【讨论】:

    • 您的来源仅适用于 Android。这是否也适用于其他 JVM?
    • 不完全确定,但同样,在大多数情况下,使用增强的 for 循环应该是默认设置。
    • 这对我来说很有意义,在访问使用数组的数据结构时摆脱所有迭代器逻辑会更快。我不知道是否快 3 倍,但肯定更快。
    【解决方案4】:

    这里隐含了答案:

    请注意,对于某些实现(例如 LinkedList 类),这些操作的执行时间可能与索引值成正比

    链表没有固有索引;调用 .get(x) 将要求列表实现找到第一个条目并调用 .next() x-1 次(对于 O(n) 或线性时间访问),其中数组支持的列表可以索引到 backingarray[x] in O(1) 或常数时间。

    如果您查看JavaDoc for LinkedList,您会看到评论

    所有操作都按照双向链表的预期执行。索引到列表中的操作将从开头或结尾遍历列表,以更接近指定索引的为准。

    JavaDoc for ArrayList有相应的

    List 接口的 Resizable-array 实现。实现所有可选列表操作,并允许所有元素,包括 null。除了实现 List 接口之外,该类还提供了一些方法来操作内部用于存储列表的数组的大小。 (这个类大致相当于Vector,只是它是不同步的。)

    sizeisEmptygetsetiteratorlistIterator 操作在恒定时间内运行。添加操作在摊销常数时间内运行,即添加 n 个元素需要 O(n) 时间。所有其他操作都以线性时间运行(粗略地说)。与LinkedList 实现相比,常数因子较低。

    related question titled "Big-O Summary for Java Collections Framework" 的答案指向此资源,"Java Collections JDK6",您可能会觉得有帮助。

    【讨论】:

      【解决方案5】:

      要找到 LinkedList 的第 i 个元素,实现会遍历所有元素,直到第 i 个。

      所以

      for(int i = 0; i < list.length ; i++ ) {
          Object something = list.get(i); //Slow for LinkedList
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-10-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-31
        • 2015-07-24
        相关资源
        最近更新 更多