为什么遍历列表比索引索引快?


125

阅读有关ADT列表Java文档时,它说:

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

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


12
另一个可以帮助您了解一般情况的示例是Joel Spolsky的文章“ Back to Basics”(搜索基本知识) -搜索“画家的算法Shlemiel”。
2012年

Answers:


211

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

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由一个简单数组支持。在这种情况下,上述两个遍历都是等效的,因为数组是连续的,因此它允许随机跳转到任意位置。


29
较小的注意事项:如果索引位于列表的后半部分,则LinkedList将从列表的末尾开始搜索,但这并没有真正改变根本的效率低下之处。它使问题减少了一点。
约阿希姆·绍尔

8
这是非常低效的。对于较大的LinkedList-是,对于较小的LinkedList-可以更快地工作 ,将其REVERSE_THRESHOLD设置为18 in java.util.Collections,很奇怪地看到这么高的答案而没有评论。
bestsss 2012年

1
@DanDiplo,如果结构为链接,则为true。但是,使用LinkedS结构是一个小谜团。它们的性能几乎总是比阵列支持的性能差很多(额外的内存占用,gc不友好和糟糕的局部性)。C#中的标准列表具有数组支持。
2012年

3
较小的注意事项:如果要检查应使用哪种迭代类型(索引还是迭代器/ foreach),则可以始终测试List是否实现RandomAccess(标记接口):List l = unknownList(); if (l instanceof RandomAccess) /* do indexed loop */ else /* use iterator/foreach */
afk5min 2012年

1
@ KK_07k11A0585:实际上,第一个示例中的增强型for循环像第二个示例中一样被编译为迭代器,因此它们是等效的。
2012年

35

答案暗示在这里:

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

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

如果您查看的JavaDocLinkedList,将会看到注释

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

JavaDoc forArrayList具有相应的

List接口的可调整大小的数组实现。实现所有可选的列表操作,并允许所有元素,包括null。除了实现List接口之外,此类还提供一些方法来操纵内部用于存储列表的数组的大小。(此类与Vector大致等效,但它是不同步的。)

sizeisEmptygetsetiterator,和listIterator操作在固定时间运行。加法运算以固定的固定时间运行,也就是说,添加n个元素需要O(n)时间。所有其他操作均以线性时间运行(大致而言)。与LinkedList实现相比,常数因子低。

名为“ Java集合框架的Big-O摘要”相关问题有一个指向该资源“ Java集合JDK6”的答案,您可能会发现有帮助。


7

尽管公认的答案肯定是正确的,但我是否可以指出一个小缺陷。报价都铎王朝:

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

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

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

资料来源:Google的Android文档,“为性能而设计”

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

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

我只想指出,在Android开发中,ArrayLists的遍历不一定相等。值得深思。


您的来源仅是Anndroid。这对于其他JVM也适用吗?
Matsemann

不能完全确定tbh,但在大多数情况下,默认使用增强的for循环。
Dhruv Gairola

这对我来说很有意义,在访问使用数组的数据结构时,摆脱所有迭代器逻辑会更快。我不知道是否快3倍,但肯定更快。
Setzer22

7

遍历一个具有偏移量的列表以进行查找,例如i,类似于画家的算法Shlemiel

Shlemiel从事街头画家的工作,在路中间画了虚线。第一天,他将一罐油漆带到公路上,完成了300码的道路。“那太好了!” 他的老板说:“你是个快劳!” 付给他一个科比

第二天,Shlemiel只完成了150码。“嗯,那还不及昨天,但是你仍然是个快劳。150码是可敬的。”

第二天,Shlemiel画了这条路的30码。“只有30个!” 叫他的老板。“那是不可接受的!第一天,你做了十倍的工作!这是怎么回事?”

“我无能为力,” Shlemiel说。“每天我都离油漆罐越来越远了!”

来源

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


4

为了找到第i个元素,LinkedList实现需要遍历所有元素直到第i个元素。

所以

for(int i = 0; i < list.length ; i++ ) {
    Object something = list.get(i); //Slow for LinkedList
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.