LinkedList 与 ArrayList 在维护有序列表方面的性能



我想保持大小为 <= 10^6 的有序List<Integer>。每次添加新元素时,我都会调用Collections.sort()方法来对列表中的新元素进行排序。据我所知,ArrayListLinkedList表现更好。但是由于我将经常调用sort()方法,因此我开始理解linkedList在对列表进行排序时会表现得更好,并且会是比ArrayList更好的选择,因为没有像ArrayList那样移动元素(使用array作为底层数据结构)。任何建议都会更有效率。

您可以使用排序列表中的Collections#binarySearch来查找正确的插入点。ArrayList可能比LinkedList表现得更好,特别是对于大尺寸,但这很容易测试。

我运行了各种方法的微观基准测试:每次插入后使用排序或二进制搜索插入正确的位置,包括ArrayList(AL)和LinkedList(LL)。我还添加了Commons TreeList和番石榴的TreeMultiset。

结论

  • 测试中最好的算法是使用 TreeMultiset ,但严格来说它不是一个列表 - 下一个最佳选择是使用 ArrayList + 二进制搜索
  • 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;
}

位桶上的完整代码。

完整结果

"基准"列包含被测方法的名称(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

由于java没有内置的multiset,这是适合您情况的完美数据结构,因此我建议使用guava库中的TreeMultiset。

多重集允许重复元素,树多集还将增加保持集合排序的好处。

LinkedList上调用 sort() 会对性能造成毁灭性的影响,因为默认实现是将List转换为数组进行排序List.sort()。在极少数情况下,使用LinkedList是有意义的,即使它看起来应该是有效的。

如果您希望始终对集合进行排序,则确实应该使用有序集合,例如TreeSet甚至PriorityQueue。它将提供更干净的代码(以及更快的排序),因为您不必担心一直自己调用sort()

如果排序是主要的性能考虑因素,则应考虑使用旨在维护顺序的数据结构。

使用普通的 Java 基类,您可以使用以下任一:

PriorityQueue (in case you want to retain duplicates)
TreeSet (filter duplicates)

无论如何,最简单的方法是对所有版本进行原型设计并运行一些基准测试+分析。

在Oracle Java/OpenJDK 7或更高版本下,两者的渐近性能是相似的。 Collections.sort将列表加载到数组中,对数组进行排序,然后通过遍历数组(使用 ListIterator)将数组加载回列表中,替换其元素。

在这两种情况下,这都是对大多数排序数组的数组排序(在 OpenJDK 7 及更高版本中O(n),因为它使用 timsort),加上两个列表迭代(在两种情况下都是O(n) - 尽管我希望LinkedList有一个更糟糕的常量项)。所以总的来说,这是一个O(n)的过程,但对LinkedList来说可能会更慢。

如果要批量插入

元素,则批量插入将总体O(n^2),这比全部插入并排序或遵循Smac89的建议使用TreeMultiset(两者都是O(n log(n)))慢。

只是为了好玩,这里有一种非常糟糕的方式来滥用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<>());

相关内容

  • 没有找到相关文章