LinkedList 与 ArrayList 在维护有序列表方面的性能
Posted
技术标签:
【中文标题】LinkedList 与 ArrayList 在维护有序列表方面的性能【英文标题】:Performance of LinkedList vs ArrayList in maintaining an ordered list 【发布时间】:2015-03-25 17:23:19 【问题描述】:我想维持一个大小 List<Integer>。每次添加新元素时,我都会调用Collections.sort()
方法对列表中的新元素进行排序。据我所知ArrayList
比LinkedList
表现更好。但是由于我会经常调用sort()
方法,所以我开始理解linkedList
在对列表进行排序时会表现得更好,并且会比ArrayList
更好,因为不会像以防万一那样移动元素的ArrayList
(使用array
作为底层数据结构)。任何更有效的建议。
【问题讨论】:
你已经回答了你的问题,链表显然更高效。另一种选择是像treeset...这样的二叉树 @Smac89 我的收藏中会有重复的元素,所以不能使用Set
,但类似于Binary Search Tree
的东西会很棒,因为它的性能会比Collections.sort()
更好。跨度>
@Smac89 为什么你认为它显然更有效率?
@assylias OP 确实说过...But since I will be calling sort() method quite often, I have come to understanding that linkedList will perform better when sorting the list and will be a better choice over ArrayList, since there is no shifting of elements as in case of ArrayList(uses array as underlying data structure).
和 ofc,因为如果操作正确,对链表进行合并排序是 O(nlogn) 操作
@Smac89 我得到的结果似乎与您的说法不符。另请注意,在 Java 中对 LinkedList 进行排序比对 ArrayList 进行排序要慢,因为前者意味着将项的附加副本复制到数组中。
【参考方案1】:
您可以在排序列表上使用Collections#binarySearch
来找到正确的插入点。 ArrayList 的性能可能比 LinkedList 更好,尤其是对于较大的尺寸,但这很容易测试。
我对各种方法进行了微基准测试:在每次插入后使用排序或使用 binarySearch 在正确的位置插入,使用 ArrayList (AL) 和 LinkedList (LL)。我还添加了 Commons TreeList 和 guava 的 TreeMultiset。
结论
测试中最好的算法是使用TreeMultiset
,但严格来说它不是一个列表 - 下一个最佳选择是使用ArrayList
+ binarySearch
ArrayList 在所有情况下都比 LinkedList 表现更好,后者需要几分钟才能完成 100,000 个元素(ArrayList 用时不到一秒)。
表现最好的代码,供参考:
@Benchmark public ArrayList<Integer> binarySearchAL()
ArrayList<Integer> list = new ArrayList<> ();
Random r = new Random();
for (int i = 0; i < n; i++)
int num = r.nextInt();
int index = Collections.binarySearch(list, num);
if (index >= 0) list.add(index, num);
else list.add(-index - 1, num);
current = list.get(0); //O(1), to make sure the sort is not optimised away
return list;
bitbucket 上的完整代码。
完整结果
“Benchmark”列包含被测方法的名称(baseLine 只是填充一个列表而不对其进行排序,其他方法有明确的名称:AL=ArrayList, LL=LinkedList,TL=Commons TreeList,treeMultiSet=guava) , (n) 是列表的大小,Score 是花费的时间,以毫秒为单位。
Benchmark (n) Mode Samples Score Error Units
c.a.p.SO28164665.baseLine 100 avgt 10 0.002 ± 0.000 ms/op
c.a.p.SO28164665.baseLine 1000 avgt 10 0.017 ± 0.001 ms/op
c.a.p.SO28164665.baseLine 5000 avgt 10 0.086 ± 0.002 ms/op
c.a.p.SO28164665.baseLine 10000 avgt 10 0.175 ± 0.007 ms/op
c.a.p.SO28164665.binarySearchAL 100 avgt 10 0.014 ± 0.001 ms/op
c.a.p.SO28164665.binarySearchAL 1000 avgt 10 0.226 ± 0.006 ms/op
c.a.p.SO28164665.binarySearchAL 5000 avgt 10 2.413 ± 0.125 ms/op
c.a.p.SO28164665.binarySearchAL 10000 avgt 10 8.478 ± 0.523 ms/op
c.a.p.SO28164665.binarySearchLL 100 avgt 10 0.031 ± 0.000 ms/op
c.a.p.SO28164665.binarySearchLL 1000 avgt 10 3.876 ± 0.100 ms/op
c.a.p.SO28164665.binarySearchLL 5000 avgt 10 263.717 ± 6.852 ms/op
c.a.p.SO28164665.binarySearchLL 10000 avgt 10 843.436 ± 33.265 ms/op
c.a.p.SO28164665.sortAL 100 avgt 10 0.051 ± 0.002 ms/op
c.a.p.SO28164665.sortAL 1000 avgt 10 3.381 ± 0.189 ms/op
c.a.p.SO28164665.sortAL 5000 avgt 10 118.882 ± 22.030 ms/op
c.a.p.SO28164665.sortAL 10000 avgt 10 511.668 ± 171.453 ms/op
c.a.p.SO28164665.sortLL 100 avgt 10 0.082 ± 0.002 ms/op
c.a.p.SO28164665.sortLL 1000 avgt 10 13.045 ± 0.460 ms/op
c.a.p.SO28164665.sortLL 5000 avgt 10 642.593 ± 188.044 ms/op
c.a.p.SO28164665.sortLL 10000 avgt 10 1182.698 ± 159.468 ms/op
c.a.p.SO28164665.binarySearchTL 100 avgt 10 0.056 ± 0.002 ms/op
c.a.p.SO28164665.binarySearchTL 1000 avgt 10 1.083 ± 0.052 ms/op
c.a.p.SO28164665.binarySearchTL 5000 avgt 10 8.246 ± 0.329 ms/op
c.a.p.SO28164665.binarySearchTL 10000 avgt 10 735.192 ± 56.071 ms/op
c.a.p.SO28164665.treeMultiSet 100 avgt 10 0.021 ± 0.001 ms/op
c.a.p.SO28164665.treeMultiSet 1000 avgt 10 0.288 ± 0.008 ms/op
c.a.p.SO28164665.treeMultiSet 5000 avgt 10 1.809 ± 0.061 ms/op
c.a.p.SO28164665.treeMultiSet 10000 avgt 10 4.283 ± 0.214 ms/op
对于 100k 项:
c.a.p.SO28164665.binarySearchAL 100000 avgt 6 890.585 ± 68.730 ms/op
c.a.p.SO28164665.treeMultiSet 100000 avgt 6 105.273 ± 9.309 ms/op
【讨论】:
+1 用于二分搜索 :)。但如果 OP 使用ArrayList
,则插入后移动需要时间。在这种情况下,LinkedList
会更有效。
我不知道,你可能是对的。在 LinkedList 上 binarySearch 可能较慢,但插入速度较快...
@TheLostMind 刚刚进行了一些测试,LinkedList 的性能对于大型样本(100k 个项目需要几分钟)来说是令人震惊的,对于较小的大小(
@TheLostMind 一旦你设置好 jmh 就不需要太多时间了——可能需要 15 分钟来编写代码,然后测试在后台运行 10 分钟左右。诚然,当我第一次使用它时,我花了超过 15 分钟来编写我的第一个现实生活测试:-)
哇,TreeMultiSet
真正展示了当您点击大量项目时 Guava 的所有优化!快九倍是没什么好闻的。【参考方案2】:
由于java没有内置multiset,这是适合您情况的完美数据结构,我建议使用guava库中的TreeMultiset。
多重集允许重复元素,并且树多重集还将增加保持集合排序的好处。
【讨论】:
TreeMultiset
确实是性能最佳的选项,即使严格来说它不是一个列表。【参考方案3】:
在LinkedList
上调用sort()
会对性能造成破坏性影响,因为List.sort()
的默认实现会将List
转换为数组进行排序。在极少数情况下使用LinkedList
是有意义的,尽管它看起来应该是有效的。
如果您希望始终对集合进行排序,则确实应该使用像TreeSet
甚至PriorityQueue
这样的有序集合。它将提供更简洁的代码(以及更快的排序),因为您不必一直担心自己调用sort()
。
【讨论】:
我希望我的列表中有重复的元素,所以不能使用Set
和 PriorityQueue
只是部分排序,所以对我的情况也没有帮助。
Collections.sort()
将列表转换为数组,然后对其进行排序。如此有效地,您将拥有O(nLogn)
复杂性.. 这是您能得到的最好的。
@MeenaChaudhary 那么ArrayList
是您最明智的选择。 LinkedList
表现非常糟糕(就像在几乎所有情况下一样)。
合并排序不需要随机访问,并且可以在O(n log n)
时间与数组一样在链表上工作。这就是std::list::sort
在 C++ 中所做的事情。 LinkedList.sort
不是专门的归并排序吗?
在 OpenJDK 上,在执行 Collections.sort
期间绝不会调用 get()
方法。它只使用toArray()
和listIterator()
。【参考方案4】:
如果排序是您的主要性能考虑因素,您应该考虑使用旨在维护顺序的数据结构。
使用普通的 Java 基类,您可以使用以下任何一种:
PriorityQueue (in case you want to retain duplicates)
TreeSet (filter duplicates)
无论如何,最简单的方法是对所有版本进行原型制作并运行一些基准测试 + 分析。
【讨论】:
【参考方案5】:在 Oracle Java / OpenJDK 7 或更高版本下,两者的渐近性能相似。 Collections.sort
将列表加载到数组中,对数组进行排序,然后通过迭代(使用ListIterator
)将数组加载回列表中,替换其中的元素。
在这两种情况下,这是对大多数排序数组的数组排序(在 OpenJDK 7 及更高版本中为 O(n)
,因为它使用 timsort),加上两次列表迭代(在这两种情况下均为 O(n)
-虽然我预计LinkedList
的常数项会更差)。所以总的来说,这是一个O(n)
进程,但LinkedList
可能会更慢。
如果您要批量插入元素,则整体插入为 O(n^2)
,这比全部插入和排序要慢,或者按照 Smac89
的建议使用 TreeMultiset
(两者都是 @ 987654331@).
只是为了好玩,这是一种滥用TreeSet
以允许它存储重复元素的真正糟糕的方式:
public class AwfulComparator<E extends Comparable<E>> implements Comparator<E>
public int compare(E o1, E o2)
int compared = o1.compareTo(o2);
return (compared == 0)?1:compared; // Never compare equal
new TreeSet<String>(new AwfulComparator<>());
【讨论】:
我很好奇,你知道为什么Collections.sort
在遇到LinkedList
时不进行合并排序吗?经过测试并认为它比使用数组实际执行的操作要慢(尤其是在 Timsort 的优势下),还是太像难以实现的工作?
@SteveJessop 我的猜测是他们想要保持代码干净——那种“if instanceof”测试是一种代码味道。我也不确定它是否会更快。要使合并排序起作用,您需要首先将列表划分为 2 个子列表,这会产生 LinkedList
s 的开销。不过,这可能不仅仅是为了获得 Timsort 的好处 - 自使用合并排序的 OpenJDK 6 以来,代码就一直如此。
“LinkedLists 有开销”——好吧,如果你把胆量从你的 LinkedList 实现中拉出来,那就不会了。例如,您可以从两端向中间工作,而不必先迭代到中间。但是知道如何直接操作 LinkedList 节点的秘密本地方法可能被合理地视为作弊。我只是想知道 Sun/Oracle 是否曾经说过任何关于它的事情,因为有不止一个合理的原因。
哦,我明白了。我刚刚找到的一个Collections.sort
实现,还在排序之前将ArrayList
复制到一个新数组中,然后复制回原始数组。所以这种复制不仅仅是对于没有随机访问的集合的开销(正如我首先想到的那样),它是将所有内容简化为Arrays.sort
。因此,它对所有集合类型都同样值得。以上是关于LinkedList 与 ArrayList 在维护有序列表方面的性能的主要内容,如果未能解决你的问题,请参考以下文章
在 ArrayList 与 LinkedList 中间插入 [重复]