【问题标题】:ArrayList Vs LinkedListArrayList 与 LinkedList
【发布时间】:2011-08-16 07:32:21
【问题描述】:

我在关注previous post,上面写着:

对于链表

  • 得到是 O(n)
  • 加起来是 O(1)
  • 删除是 O(n)
  • Iterator.remove 是 O(1)

对于数组列表

  • 得到是 O(1)
  • add 摊销为 O(1),但最坏情况为 O(n),因为必须调整和复制数组的大小
  • 删除是 O(n)

因此,通过查看这一点,我得出结论,如果我只需要在我的集合中按顺序插入 5000000 个元素,LinkedList 将优于 ArrayList

如果我只需要通过迭代从集合中获取元素,即不抓取中间的元素,LinkedList 仍然会超越 `ArrayList.

现在为了验证我上面的两个陈述,我写了下面的示例程序......但我很惊讶我上面的陈述被证明是错误的。

ArrayList 在这两种情况下都优于 Linkedlist。从 Collection 中添加和获取它们所花费的时间比 LinkedList 少。是不是我做错了什么,或者关于 LinkedListArrayList 的初始声明对于大小为 5000000 的集合不适用?

我提到了大小,因为如果我将元素数量减少到 50000 个,LinkedList 的性能会更好,并且初始语句成立。

long nano1 = System.nanoTime();

List<Integer> arr = new ArrayList();
for(int i = 0; i < 5000000; ++i) {
    arr.add(i);
}
System.out.println( (System.nanoTime() - nano1) );

for(int j : arr) {
    ;
}
System.out.println( (System.nanoTime() - nano1) );

long nano2 = System.nanoTime();

List<Integer> arrL = new LinkedList();
for(int i = 0; i < 5000000; ++i) {
    arrL.add(i);
}
System.out.println( (System.nanoTime() - nano2) );

for(int j : arrL) {
    ;
}
System.out.println( (System.nanoTime() - nano2) );

【问题讨论】:

  • 对于你的 LL,你是加头还是加尾?
  • 另外请记住,如果您知道 ArrayList 需要保存多少个元素,则可以为 ArrayList 指定initialCapacity,这消除了O(n) 最坏的情况。
  • @Andrew 好点;如果您对数组大小有一个合理的下限,这总是一个好主意。
  • 您说的是“顺序插入”,但上面的基准和引用是关于.add,它是一个追加(例如插入,但总是在最后一个元素之后)。插入意味着“在任何地方插入”,在讨论数据结构的操作成本时完全不同。

标签: java data-structures collections arraylist linked-list


【解决方案1】:

请记住,大 O 复杂度描述的是渐近行为,可能无法反映实际的实现速度。它描述了每个操作的成本如何随着列表的大小而不是每个操作的速度而增长。例如,add 的以下实现是 O(1) 但并不快:

public class MyList extends LinkedList {
    public void add(Object o) {
        Thread.sleep(10000);
        super.add(o);
    }
}

我怀疑在您的情况下 ArrayList 表现良好,因为它相当积极地增加了它的内部缓冲区大小,因此不会有大量的重新分配。当缓冲区不需要调整大小时,ArrayList 会有更快的adds。

在进行此类分析时,您还需要非常小心。我建议您更改分析代码以进行预热阶段(因此 JIT 有机会在不影响结果的情况下进行一些优化)并在多次运行中平均结果。

private final static int WARMUP = 1000;
private final static int TEST = 1000;
private final static int SIZE = 500000;

public void perfTest() {
    // Warmup
    for (int i = 0; i < WARMUP; ++i) {
        buildArrayList();
    }
    // Test
    long sum = 0;
    for (int i = 0; i < TEST; ++i) {
        sum += buildArrayList();
    }
    System.out.println("Average time to build array list: " + (sum / TEST));
}

public long buildArrayList() {
    long start = System.nanoTime();
    ArrayList a = new ArrayList();
    for (int i = 0; i < SIZE; ++i) {
        a.add(i);
    }
    long end = System.nanoTime();
    return end - start;
}

... same for buildLinkedList

(请注意sum 可能会溢出,您最好使用System.currentTimeMillis())。

也有可能编译器正在优化你的空 get 循环。确保循环确实做了一些事情来确保调用正确的代码。

【讨论】:

  • 很好的解释卡梅伦 - 我想我也在下面添加了一些好东西。我会在您的示例中使用 (size) 预先构建数组列表
  • @MJB:谢谢!你的回答也不错。我没有预先构建该列表,因为在实践中知道您正在处理的数据的确切大小(需要引用)相对较少,所以我认为在不对数据做太多假设的情况下进行基准测试很有用。也就是说,除非您完全确定用例是什么,否则基准测试可能应该使用预先构建的和自动分配的缓冲区运行。
【解决方案2】:

这是一个糟糕的 IMO 基准。

  • 需要在循环中重复多次来预热 jvm
  • 需要在您的迭代循环中做一些事情,否则可以优化数组
  • ArrayList 调整大小,代价高昂。如果您将ArrayList 构建为new ArrayList(500000),您将一口气构建,然后所有分配都会非常便宜(一个预分配支持数组)
  • 您没有指定您的内存 JVM - 它应该使用 -xMs == -Xmx 运行(所有内容都预先分配)并且足够高,以至于不会触发任何 GC
  • 此基准测试并未涵盖 LinkedList 最令人不快的方面 - 随机访问。 (迭代器不一定是同一件事)。如果您将大型集合大小的 10% 作为list.get 的随机选择提供,您会发现链表对于抓取除第一个或最后一个元素之外的任何内容都很糟糕。

对于数组列表:jdk get 是您所期望的:

public E get(int index) {
    RangeCheck(index);

    return elementData[index];
}

(基本上只返回索引的数组元素。,

对于链表:

public E get(int index) {
    return entry(index).element;
}

看起来很相似?不完全的。 entry 是一个方法而不是原始数组,看看它必须做什么:

private Entry<E> entry(int index) {
    if (index < 0 || index >= size)
        throw new IndexOutOfBoundsException("Index: "+index+
                                            ", Size: "+size);
    Entry<E> e = header;
    if (index < (size >> 1)) {
        for (int i = 0; i <= index; i++)
            e = e.next;
    } else {
        for (int i = size; i > index; i--)
            e = e.previous;
    }
    return e;
}

没错,如果你要求说list.get(250000),它必须从头部开始并反复迭代下一个元素。大约 250000 次访问(代码中有一个优化,它从头部或尾部开始,具体取决于哪个访问次数较少。)

【讨论】:

  • 是的,当我运行一个(不是很好的基准测试,但考虑到这些)时,只要我预先构建 ArrayList,我就能始终更快地获得 ArrayList。在少数情况下,构建 arraylist 结构(空)的时间开始产生一些影响。我很乐意分享我的程序,但是我是第一个承认的,我不擅长编写这些 JVM 基准测试——由于我上面提到的一些原因,它们确实会产生可怕的误导。
  • 正如您指出 Java 存在许多不确定性行为:JIT 编译、GC 等等。基准测试必须用一粒盐来进行,但有时进行计时会很有用,看看“方法 x 在大多数情况下是否比方法 y 快”
  • 相信我,我并不是在批评海报的意图。检查假设是很好的。我只是想指出,在 Java 中很难正确执行基准测试,尤其是考虑到 JVM 预热和优化。
  • 请注意,OP 特别提到只需要迭代器访问列表。您关于随机访问的观点是正确的,但对于这个特定的基准测试并不那么重要。在一般情况下,您是对的:如果您需要随机访问,则不要使用“LinkedList”。
  • 是的,我看到了,但我仍然想(迂腐地)说明一点。许多菜鸟都读过 SO,而 LinkedList 的随机访问缓慢确实是 IMO 在决定使用哪个时遇到的最大问题
【解决方案3】:

ArrayList 是一种比 LinkedList 更简单的数据结构。 ArrayList 在连续的内存位置有一个指针数组。仅当数组扩展超出其分配的大小时,才需要重新创建它。

LinkedList 由一系列节点组成;每个节点都是分开分配的,并且有指向其他节点的前后指针。

那么这是什么意思?除非您需要在中间插入、拼接、在中间删除等。ArrayList 通常会更快。它需要更少的内存分配,具有更好的引用局部性(这对于处理器缓存很重要)等。

【讨论】:

    【解决方案4】:

    要了解为什么您得到的结果与“大 O”特征不矛盾。我们需要回到首要原则;即the definition

    令 f(x) 和 g(x) 是定义在实数的某个子集上的两个函数。一个人写

    f(x) = O(g(x)) as x -> infinity
    

    当且仅当,对于足够大的 x 值,f(x) 至多是一个常数乘以 g(x) 的绝对值。也就是说,f(x) = O(g(x)) 当且仅当存在一个正实数 M 和一个实数 x0 使得

    |f(x)| <= M |g(x)| for all x > x_0.
    

    在许多情况下,当变量 x 趋于无穷大时,我们对增长率感兴趣的假设没有说明,更简单的写法是 f(x) = O(g(x))。

    因此,add1 is O(1) 语句意味着对大小为 N 的列表执行add1 操作的时间成本趋于常数 Cadd1,因为 N 趋于无穷大。 p>

    声明add2 is O(1) amortized over N operations,意味着N个add2操作序列之一的平均时间成本趋向于一个常数Cadd2为N趋于无穷。

    没有说的是那些常量 Cadd1 和 Cadd2 是什么。事实上,LinkedList 在基准测试中比 ArrayList 慢的原因是 Cadd1 大于 Cadd2

    教训是大 O 表示法不能预测绝对甚至相对性能。它所预测的只是性能函数的形状,因为控制变量变得非常大。了解这一点很有用,但它并不能告诉你你需要知道的一切。

    【讨论】:

      【解决方案5】:

      1) 底层数据结构 ArrayList 和 LinkedList 之间的第一个区别在于 ArrayList 由 Array 支持,而 LinkedList 由 LinkedList 支持。这将导致性能上的进一步差异。

      2) LinkedList 实现双端队列 ArrayList 和 LinkedList 的另一个区别是除了 List 接口之外,LinkedList 还实现了 Deque 接口,该接口为 add() 和 poll() 以及其他几个 Deque 函数提供先进先出操作。 3) 在ArrayList中添加元素 在 ArrayList 中添加元素是 O(1) 操作,如果它不触发 Array 的 re-size,在这种情况下它变成 O(log(n)),另一方面在 LinkedList 中添加一个元素是 O(1) 操作,因为它不需要任何导航。

      4) 从某个位置移除元素 为了从特定索引中删除元素,例如通过调用 remove(index),ArrayList 执行复制操作,使其接近 O(n),而 LinkedList 需要遍历该点,这也使其成为 O(n/2),因为它可以根据接近度从任一方向遍历.

      5) 遍历 ArrayList 或 LinkedList 迭代是 LinkedList 和 ArrayList 的 O(n) 操作,其中 n 是元素的数字。

      6) 从某个位置检索元素 get(index) 操作在 ArrayList 中为 O(1),而在 LinkedList 中为 O(n/2),因为它需要遍历该条目。不过,在大 O 表示法中,O(n/2) 只是 O(n),因为我们忽略了那里的常数。

      7) 内存 LinkedList 使用一个包装对象 Entry,它是一个静态嵌套类,用于存储数据和两个节点 next 和 previous,而 ArrayList 只是将数据存储在 Array 中。

      因此,在 ArrayList 的情况下,内存需求似乎比 LinkedList 少,除了 Array 在将内容从一个 Array 复制到另一个 Array 时执行重新调整大小操作的情况。

      如果 Array 足够大,此时可能会占用大量内存并触发垃圾收集,这会减慢响应时间。

      从 ArrayList 与 LinkedList 之间的所有上述差异来看,看起来 ArrayList 在几乎所有情况下都是比 LinkedList 更好的选择,除非您执行频繁的 add() 操作而不是 remove() 或 get()。

      修改链表比修改 ArrayList 更容易,尤其是当您从开头或结尾添加或删除元素时,因为链表在内部保留了这些位置的引用,并且可以在 O(1) 时间内访问它们。

      也就是说,你不需要遍历链表到达你想要添加元素的位置,那样的话,加法就变成了O(n)操作。例如,在链表中间插入或删除一个元素。

      在我看来,使用 ArrayList 而不是 LinkedList 在 Java 中的大部分实际用途。

      【讨论】:

        【解决方案6】:

        大 O 表示法不是关于绝对时间,而是关于相对时间,您无法将一种算法的数量与另一种算法的数量进行比较。

        您只能了解同一算法如何对增加或减少的元组数量做出反应。

        一个算法一次操作可能需要一个小时,两次操作可能需要2小时,并且是O(n),另一种也是O(n),一次操作需要1毫秒,两次操作需要2毫秒.

        如果使用 JVM 进行测量,另一个问题是热点编译器的优化。 JIT 编译器可能会消除无操作循环。

        要考虑的第三件事是操作系统和 JVM,它们同时使用缓存并运行垃圾收集。

        【讨论】:

          【解决方案7】:

          很难为 LinkedList 找到一个好的用例。如果你只需要使用 Dequeu 接口,你可能应该使用 ArrayDeque。如果你真的需要使用 List 接口,你会经常听到总是使用 ArrayList 的建议,因为 LinkedList 在访问随机元素时表现得很糟糕。

          不幸的是,如果必须删除或插入列表开头或中间的元素,ArrayList 也会出现性能问题。

          然而,有一个名为 GapList 的新列表实现,它结合了 ArrayList 和 LinkedList 的优点。它被设计为 ArrayList 和 LinkedList 的直接替代品,因此实现了接口 List 和 Deque。 ArrayList 提供的所有公共方法也都实现了(ensureCapacty、trimToSize)。

          GapList 的实现保证了通过索引对元素进行有效的随机访问(如 ArrayList 所做的那样),同时在列表的头部和尾部高效地添加和删除元素(如 LinkedList 所做的那样)。

          您可以在http://java.dzone.com/articles/gaplist-%E2%80%93-lightning-fast-list 找到有关 GapList 的更多信息。

          【讨论】:

            【解决方案8】:

            O 符号分析提供了重要的信息,但它有其局限性。根据定义,O 符号分析认为每个操作的执行时间大致相同,这是不正确的。正如@seand 指出的那样,链表在内部使用更复杂的逻辑来插入和获取元素(查看源代码,您可以在 IDE 中按 ctrl+click)。 ArrayList 内部只需要将元素插入数组并偶尔增加其大小(即使是 o(n) 操作,实际上也可以很快完成)。

            干杯

            【讨论】:

              【解决方案9】:

              您可以将添加或删除作为两步操作分开。

              LinkedList:如果你添加一个元素到索引n,你可以将指针从0移动到n-1,然后你可以执行你所谓的O(1)添加操作。删除操作是一样的。


              ArraryList:ArrayList 实现了 RandomAccess 接口,这意味着它可以在 O(1) 内访问一个元素。
              如果你在索引 n 中添加一个元素,它可以到 O(1) 中的第 n-1 索引,将元素移动到 n-1 之后,添加设置元素在 n 槽中。
              移动操作由称为System.arraycopy 的本机方法执行,速度非常快。

              public static void main(String[] args) {
              
                  List<Integer> arrayList = new ArrayList<Integer>();
                  for (int i = 0; i < 100000; i++) {
                      arrayList.add(i);
                  }
              
                  List<Integer> linkList = new LinkedList<Integer>();
              
                  long start = 0;
                  long end = 0;
                  Random random = new Random();
              
                  start = System.currentTimeMillis();
                  for (int i = 0; i < 10000; i++) {
                      linkList.add(random.nextInt(100000), 7);
                  }
                  end = System.currentTimeMillis();
                  System.out.println("LinkedList add ,random index" + (end - start));
              
                  start = System.currentTimeMillis();
                  for (int i = 0; i < 10000; i++) {
                      arrayList.add(random.nextInt(100000), 7);
                  }
                  end = System.currentTimeMillis();
                  System.out.println("ArrayList add ,random index" + (end - start));
              
                  start = System.currentTimeMillis();
                  for (int i = 0; i < 10000; i++) {
                      linkList.add(0, 7);
                  }
                  end = System.currentTimeMillis();
                  System.out.println("LinkedList add ,index == 0" + (end - start));
              
                  start = System.currentTimeMillis();
                  for (int i = 0; i < 10000; i++) {
                      arrayList.add(0, 7);
                  }
                  end = System.currentTimeMillis();
                  System.out.println("ArrayList add ,index == 0" + (end - start));
              
                  start = System.currentTimeMillis();
                  for (int i = 0; i < 10000; i++) {
                      linkList.add(i);
                  }
                  end = System.currentTimeMillis();
                  System.out.println("LinkedList add ,index == size-1" + (end - start));
              
                  start = System.currentTimeMillis();
                  for (int i = 0; i < 10000; i++) {
                      arrayList.add(i);
                  }
                  end = System.currentTimeMillis();
                  System.out.println("ArrayList add ,index == size-1" + (end - start));
              
                  start = System.currentTimeMillis();
                  for (int i = 0; i < 10000; i++) {
                      linkList.remove(Integer.valueOf(random.nextInt(100000)));
                  }
                  end = System.currentTimeMillis();
                  System.out.println("LinkedList remove ,random index" + (end - start));
              
                  start = System.currentTimeMillis();
                  for (int i = 0; i < 10000; i++) {
                      arrayList.remove(Integer.valueOf(random.nextInt(100000)));
                  }
                  end = System.currentTimeMillis();
                  System.out.println("ArrayList remove ,random index" + (end - start));
              
                  start = System.currentTimeMillis();
                  for (int i = 0; i < 10000; i++) {
                      linkList.remove(0);
                  }
                  end = System.currentTimeMillis();
                  System.out.println("LinkedList remove ,index == 0" + (end - start));
              
                  start = System.currentTimeMillis();
                  for (int i = 0; i < 10000; i++) {
                      arrayList.remove(0);
                  }
                  end = System.currentTimeMillis();
                  System.out.println("ArrayList remove ,index == 0" + (end - start));
              
              }
              

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2012-08-17
                • 1970-01-01
                • 2014-12-31
                • 2016-12-19
                • 1970-01-01
                • 2017-10-24
                • 1970-01-01
                • 2013-10-17
                相关资源
                最近更新 更多