如果在列表中间插入,LinkedList是否真的比ArrayList快?

时间:2013-05-29 08:19:12

标签: java collections

- LinkedListArrayList之间有什么区别?什么时候最好使用LinkedList

我认为每个Java开发人员至少在访谈时都听过一次这个问题。

- 如果您希望能够在列表中间插入项目,则首选链接列表。

这是这个问题的常见答案。大家都知道。每当你问一个关于List实现之间差异的问题时,你会得到如下答案:

  

我什么时候应该使用LinkedList?什么时候需要在元素之间或开始时有效删除?

From here

  

忘记提及插入费用。在LinkedList中,一旦你有正确的位置,插入成本O(1),而在ArrayList中它会上升到O(n) - 必须移动经过插入点的所有元素。

From here

  

当您希望能够在列表中间插入项目(例如优先级队列)时,链接列表优于数组。

From here

  

ArrayList较慢,因为它需要复制部分数组才能删除已经空闲的插槽。 LinkedList只需要操作几个引用。

From here

还有更多......

但你有没有试过自己复制它?我昨天试过并得到了这些结果:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String... args) {
        final int MAX_VAL = 10000;
        List<Integer> linkedList = new LinkedList<Integer>();
        List<Integer> arrayList = new ArrayList<Integer>();
        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
            arrayList.add(i);
        }
        long time = System.nanoTime();
        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(MAX_VAL/2, i);
        }
        System.out.println("LL time: " + (System.nanoTime() - time));
        time = System.nanoTime();
        for(int i = 0; i < MAX_VAL; i++) {
            arrayList.add(MAX_VAL/2, i);
        }
        System.out.println("AL time: " + (System.nanoTime() - time));
    }
}

输出:

  

LL时间:114098106

     

AL时间:24121889

那是什么?为什么LinkedList太吸引人了?也许我们应该尝试删除操作而不是添加?好的,我们试试吧:

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Test {
    public static void main(String... args) {
        final int MAX_VAL = 10000;
        List<Integer> linkedList = new LinkedList<Integer>();
        List<Integer> arrayList = new ArrayList<Integer>();
        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
            arrayList.add(i);
        }
        long time = System.nanoTime();
        for(int i = 0; i < MAX_VAL/2; i++) {
            linkedList.remove(MAX_VAL/2);
        }
        System.out.println("LL time: " + (System.nanoTime() - time));
        time = System.nanoTime();
        for(int i = 0; i < MAX_VAL/2; i++) {
            arrayList.remove(MAX_VAL/2);
        }
        System.out.println("AL time: " + (System.nanoTime() - time));
    }
}

输出:

  

LL时间:27581163

     

AL时间:3103051

哦,ArrayList仍然比LinkedList快。是什么原因?这个神话被破坏了吗?或者我错了?

enter image description here

8 个答案:

答案 0 :(得分:27)

  

BUSTED

不是真的。这里

for(int i = 0; i < MAX_VAL; i++) {
    linkedList.add(MAX_VAL/2, i);
}

你不只是插入物品;您每次都要支付从开头到i的迭代费用。当然,那是O(i)

另一方面,在您真正见证中间列表插入的性能优势之前,列表必须非常大。 System.arraycopy是一个超快速的操作,另一方面,每次插入LinkedList都需要分配一个节点实例。

总之,对于99%或更多的实际案例,ArrayList是更好的选择,并且利用LinkedList的狭隘优势需要非常谨慎。

关于对JVM进行微基准测试的一般说明

我还应警告您,您的基准测试代码严重不足。在JVM上进行微观问答时,需要注意相当大的事项清单,例如:

  • 总是热身代码让JIT编译器得到它;
  • 由于准确性/精确度问题而非常谨慎地解释nanoTime结果。使读数增长至少几毫秒(百万纳秒)以确保可靠性;
  • 控制垃圾收集器的虚假副作用;

因此,建议使用现成的微基准测试框架,例如OpenJDK's jmh

答案 1 :(得分:8)

为了演示add()操作的有效性,最好使用ListIterator对象而不是list对象。如果直接在链表上使用add()方法,它将从列表头开始,并且必须迭代到要插入项的位置。这部分需要O( n )。如果使用ListIterator,它将保持我们添加元素的位置,并且算法不必每次都迭代到列表的中间。

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

public class Test {
    public static void main(String... args) {
        final int MAX_VAL = 10000;
        List<Integer> linkedList = new LinkedList<Integer>();
        List<Integer> arrayList = new ArrayList<Integer>();
        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
            arrayList.add(i);
        }
        long time = System.nanoTime();


        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(MAX_VAL/2, i);
        }
        System.out.println("LL time:\t" + (System.nanoTime() - time));

        time = System.nanoTime();
        for(int i = 0; i < MAX_VAL; i++) {
            arrayList.add(MAX_VAL/2, i);
        }
        System.out.println("AL time:\t" + (System.nanoTime() - time));


        //Reset the lists
        linkedList = new LinkedList<Integer>();
        arrayList = new ArrayList<Integer>();
        for(int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
            arrayList.add(i);
        }

        time = System.nanoTime();
        ListIterator<Integer> li = linkedList.listIterator(MAX_VAL/2);
        for(int i = 0; i < MAX_VAL; i++) {
            li.add(i);
        }
        System.out.println("LL iterator:\t" + (System.nanoTime() - time));

        time = System.nanoTime();
        ListIterator<Integer> ali = arrayList.listIterator(MAX_VAL/2);
        for(int i = 0; i < MAX_VAL; i++) {
            ali.add(i);
        }
        System.out.println("AL iterator:\t" + (System.nanoTime() - time));
    }
}

我的结果显示在LinkedList上使用ListIterator可以在“中间”中插入元素提供最佳性能:

LL time:     237819474
AL time:      31410507
LL iterator:   5423172
AL iterator:  23975798

答案 2 :(得分:4)

您的测试存在偏差 - 它不会衡量通常的性能差异。

关于LinkedList结构的一般观察(与ArrayList相比,对于大型列表):

  1. 添加/删除头部或尾部的节点非常快
  2. 从中间获取元素非常慢
  3. 当您接近列表的任何一端时,获取元素会变得更快(线性)
  4. 从头部或尾部获取元素接近ArrayList的速度
  5. 在中间的某处添加/删除元素是两个操作:get plus node insertion
  6. 如果您使用ListIterator,您可以在中间某处添加/删除节点并避免获取 - 一个非常快速的操作
  7. 您的测试打算测试(5)。

    但它始终执行最坏的情况 - 在中间添加/删除元素。

    您的微基准会出现系统错误。您需要统一或随机分发添加/删除位置。或者使用真实复杂的&amp; amp;进行宏观基准测试具有挑战性的应用程序。

    有关创建准确微观基准的挑战的有趣读物:Java theory and practice: Anatomy of a flawed microbenchmark

答案 3 :(得分:1)

我重写了Matej的程序,随机选择一个方法并为每种方法运行50个试验的数组。如果您在每个类别中取平均最快一半的试验,那么结果如下:

LL:570
AL:120
LL迭代器:1
AL迭代器:60

LL迭代器确实需要很多分拣时间。在最坏的情况下,由于预热(第一个周期)和gc(未分类数据的随机尖峰),它的性能下降了15倍。

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Random;

public class TestList {

    public static void main(String... args) {
        final int MAX_VAL = 10000;
        int[] currentIndex = {0, 0, 0, 0};
        int[] remaining = {50, 50, 50, 50};
        int[][] sequence = new int[4][50];

        while (keepWorking(remaining)) { //run 50 tests for each case at random

            int currentMethod = chooseMethod(remaining); //choose case. Probability is higher for tests with less trials

            switch (currentMethod) { //run a test based on the choice
                case 0:
                    sequence[currentMethod][currentIndex[currentMethod]] = getLL(MAX_VAL);
                    break;
                case 1:
                    sequence[currentMethod][currentIndex[currentMethod]] = getAL(MAX_VAL);
                    break;
                case 2:
                    sequence[currentMethod][currentIndex[currentMethod]] = getLLIt(MAX_VAL);
                    break;
                default:
                    sequence[currentMethod][currentIndex[currentMethod]] = getALIt(MAX_VAL);
                    break;
            }

            remaining[currentMethod]--;
            currentIndex[currentMethod]++;
        }

        for (int[] ar : sequence) {
            Arrays.sort(ar);
        }

        System.out.println("Time (us\nLL    \tAL\tLL incr\t AL incr");
        for (int i = 0; i < sequence[0].length; i++) {
            System.out.println(sequence[0][i] + "\t" + sequence[1][i] + "\t" + sequence[2][i] + "\t" + sequence[3][i]);
        }
        System.out.println("\nTime normalized to fastest run of a method\nLL\tAL\tLL incr\t AL incr");
        for (int i = 0; i < sequence[0].length; i++) {
            System.out.print(i);
            for (int j = 0; j < sequence.length; j++) {  //to 4
                int a = sequence[j][i] / (sequence[j][0]/100); //to keep result within the scope of int
                System.out.print("\t" + a);
            }
            System.out.println();
        }
    }

    public static boolean keepWorking(int[] remaining) {

        for (int i = 0; i < remaining.length; i++) {
            if (remaining[i] > 0) {
                return true;
            }
        }
        return false;
    }

    public static int chooseMethod(int[] rem) {
        int[] bins = new int[rem.length];
        for (int i = 0; i < rem.length; i++) {
            for (int j = i; j < rem.length; j++) {
                bins[j] += rem[i];
            }
        }
        int randomNum = new Random().nextInt(bins[rem.length - 1]);
        for (int i = 0; i < bins.length; i++) {
            if (randomNum < bins[i]) {
                return i;
            }
        }
        return -1;
    }

    public static int getLL(int MAX_VAL) {

        List<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
        }
        long time = System.nanoTime();

        for (int i = 0; i < MAX_VAL; i++) {
            linkedList.add(MAX_VAL / 2, i);
        }
        return (int) (System.nanoTime() - time)/1000;
    }

    public static int getAL(int MAX_VAL) {

        List<Integer> arrayList = new ArrayList<>(MAX_VAL);
        for (int i = 0; i < MAX_VAL; i++) {
            arrayList.add(i);
        }
        long time = System.nanoTime();
        for (int i = 0; i < MAX_VAL; i++) {
            arrayList.add(MAX_VAL / 2, i);
        }
        return (int) (System.nanoTime() - time)/1000;
    }

    public static int getLLIt(int MAX_VAL) {

        List<Integer> linkedList = new LinkedList<>();
        for (int i = 0; i < MAX_VAL; i++) {
            linkedList.add(i);
        }

        long time = System.nanoTime();

        ListIterator<Integer> li = linkedList.listIterator(MAX_VAL / 2);
        for (int i = 0; i < MAX_VAL; i++) {
            li.add(i);
        }
        return (int) (System.nanoTime() - time)/1000;
    }

    public static int getALIt(int MAX_VAL) {

        List<Integer> arrayList = new ArrayList<>(MAX_VAL);
        for (int i = 0; i < MAX_VAL; i++) {
            arrayList.add(i);
        }

        long time = System.nanoTime();
        ListIterator<Integer> ali = arrayList.listIterator(MAX_VAL / 2);
        for (int i = 0; i < MAX_VAL; i++) {
            ali.add(i);
        }
        return (int) (System.nanoTime() - time)/1000;
    }
}

答案 4 :(得分:0)

必须谨慎对待这样简单的分析:

  • 垃圾收集可能会在不可预测的时间发生,从而减慢垃圾收集速度 不可预测的部分。
  • JRE首次启动时速度较慢,后来“预热”。

要解决此问题,请在循环中进行分析,以随机顺序多次重复这两种情况,并采用典型值而不是极值。这有时会产生不同的结果。

答案 5 :(得分:0)

因为ArrayList按顺序存储值 从而 1:更快地添加值(只需在最后一个索引中添加值) 2:更新或删除速度较慢(在到达节点之前必须遍历整个列表)

因为数组列表适用于LinkedList概念 1:插入速度较慢(需要找到对prev或next值的引用) 2:更新速度更快(因为只能通过引用到达确切的节点)

this链接可以参考

答案 6 :(得分:0)

在理想情况下,您始终会插入到排序列表中。首先,使用二进制搜索机制找到插入索引,然后在该索引处插入。此外,在执行此操作时,您无法始终使用相同的列表器。您将迭代器设置为新索引位置evrytime。所以在现实生活中,插入更快。

答案 7 :(得分:0)

Oof... 人可以变得多么教条...

在 YouTube 上查找“Bjarne Stroustrup:为什么应该避免链接列表”以获得所有答案。答案不是来自一些随机的 StackOverflow 海报,而是来自一位计算机科学教授,他 - 顺便说一下 - 开发了 C++。

保持简短:

  • 链表最大限度地提高了缓存未命中率,因为节点在堆上随机分配,还可以防止硬件和软件缓存预取
  • 将一个节点读入缓存往往会浪费缓存线宽,这也最大限度地浪费了内存带宽,这就是 Von-Neumann-Bottleneck
  • 链表存储更多数据(指针),从而最大限度地浪费内存带宽,这就是冯诺依曼瓶颈
  • 特别是对于 SIMD,流式数组非常快,这将使任意大小的数组的 memcpy 比遍历相同大小的链表更快,并且具有上面列出的所有负面影响。在现代(1980+????)机器上插入/删除数组总是更快。

链表非常慢。这是一种易于理解的“高级”数据结构,即使面对确凿的证据,前计算机科学专业的学生仍坚持使用它们,如答案所示。我个人认为这很尴尬。

当 CPU 没有缓存时,链表会更快 - 当内存与 CPU 一样快/快时。大概就在那个时候,教授前面提到的学生的教授停止学习了。