ArrayList与LinkedList

时间:2011-05-01 03:15:52

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

我在关注previous post后说:

  

对于LinkedList

     
      
  • get is O(n)
  •   
  • add is O(1)
  •   
  • 删除是O(n)
  •   
  • Iterator.remove是O(1)
  •   
     

对于ArrayList

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

因此,通过观察这一点,我得出结论,如果我要在我的集合中为5000000个元素执行顺序插入,LinkedList将超出ArrayList

如果我只是通过迭代来获取集合中的元素,即不在中间抓取元素,那么LinkedList将超出`ArrayList。

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

在这两种情况下,

ArrayList都超过Linkedlist。添加以及从Collection中获取它们所需的时间比LinkedList少。{1}}。有什么我做错了,或者关于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) );

9 个答案:

答案 0 :(得分:50)

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

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

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

进行此类分析时,您还需要非常小心。我建议您更改分析代码以进行预热阶段(因此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循环。确保循环确实能够确保调用正确的代码。

答案 1 :(得分:20)

这是一个糟糕的基准IMO。

  • 需要循环重复多次以预热jvm
  • 需要在迭代循环中做某事或者可以优化数组
  • ArrayList调整大小,费用很高。如果你构造了ArrayList作为new ArrayList(500000),你将在一次打击中构建,然后所有分配都相当便宜(一个预分配后备阵列)
  • 您没有指定内存JVM - 它应该使用-xMs == -Xmx(所有预先分配的内容)运行并且足够高以至于不会触发GC
  • 此基准测试未涵盖LinkedList最令人不愉快的方面 - 随机访问。 (迭代器不一定是同一个东西)。如果您将大型集合的大小的10%作为list.get的随机选择,则会发现链接列表很难抓取除第一个或最后一个元素之外的任何内容。

对于一个arraylist: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次访问左右(代码中的优化是从头部或尾部开始,具体取决于访问次数较少。)

答案 2 :(得分:12)

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

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

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

答案 3 :(得分:6)

要理解为什么你得到的结果与“大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操作的时间成本倾向于常数C add1 ,因为N倾向于无穷大。

语句add2 is O(1) amortized over N operations,意味着N add2个操作序列之一的平均时间成本倾向于常数C add2 因为N倾向于无穷大。

什么是不说那些常数C add1 和C add2 是什么。实际上,LinkedList比基准中的ArrayList慢的原因是C add1 大于C add2

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

答案 4 :(得分:1)

大O符号不是关于绝对时间,而是关于相对时间,你无法比较一种算法与另一种算法的数量。

您只能获得相同算法如何对增加或减少元组数量做出反应的信息。

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

使用JVM进行测量的另一个问题是热点编译器的优化。 JIT编译器可能会消除do-nothing-loop循环。

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

答案 5 :(得分:1)

很难为LinkedList找到一个好的用例。如果您只需要使用Dequeu接口,则应该使用ArrayDeque。如果你真的需要使用List接口,你会经常听到使用Always 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的更多信息。

答案 6 :(得分:1)

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

2)LinkedList实现了Deque ArrayList和LinkedList之间的另一个区别是,除了List接口之外,LinkedList还实现了Deque接口,它为add()和poll()以及其他几个Deque函数提供先进先出操作。 3)在ArrayList中添加元素 在ArrayList中添加元素是O(1)操作,如果它不触发Array的重新大小,在这种情况下它变为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),因为它需要遍历到该条目。虽然,在Big O表示法O(n / 2)只是O(n),因为我们忽略那里的常数。

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

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

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

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

修改链表比ArrayList更容易,特别是如果要从开始或结束添加或删除元素,因为链表在内部保留了对这些位置的引用,并且可以在O(1)时间内访问它们。

换句话说,您不需要遍历链表以到达要添加元素的位置,在这种情况下,添加变为O(n)操作。例如,在链接列表的中间插入或删除元素。

在我看来,对于Java中的大多数实际用途,使用LinkedList而不是LinkedList。

答案 7 :(得分:0)

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

干杯

答案 8 :(得分:0)

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

  

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));

}