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() 方法对列表中的新元素进行排序。据我所知ArrayListLinkedList 表现更好。但是由于我会经常调用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()

【讨论】:

我希望我的列表中有重复的元素,所以不能使用 SetPriorityQueue 只是部分排序,所以对我的情况也没有帮助。 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 个子列表,这会产生 LinkedLists 的开销。不过,这可能不仅仅是为了获得 Timsort 的好处 - 自使用合并排序的 OpenJDK 6 以来,代码就一直如此。 “LinkedLists 有开销”——好吧,如果你把胆量从你的 LinkedList 实现中拉出来,那就不会了。例如,您可以从两端向中间工作,而不必先迭代到中间。但是知道如何直接操作 LinkedList 节点的秘密本地方法可能被合理地视为作弊。我只是想知道 Sun/Oracle 是否曾经说过任何关于它的事情,因为有不止一个合理的原因。 哦,我明白了。我刚刚找到的一个Collections.sort 实现,还在排序之前将ArrayList 复制到一个新数组中,然后复制回原始数组。所以这种复制不仅仅是对于没有随机访问的集合的开销(正如我首先想到的那样),它是将所有内容简化为Arrays.sort。因此,它对所有集合类型都同样值得。

以上是关于LinkedList 与 ArrayList 在维护有序列表方面的性能的主要内容,如果未能解决你的问题,请参考以下文章

Linkedlist与arraylist的比较[重复]

ArrayList与LinkedList常见的问题

在 ArrayList 与 LinkedList 中间插入 [重复]

ArrayList与LinkedList

Java中arraylist和linkedlist源码分析与性能比较

arraylist与linkedlist的区别与性能测试