Java

你不知道的LinkedList(二):LinkedList的增删

2020-08-17  本文已影响0人  冬天里的懒喵

十年前,在刚解除java不久,面试中就有人问道LinkedList和ArrayList的区别。我记得当时普片的答案都说是,LinikedList底层基于链表实现。而ArrayList底层基于数据。因此LinkedList的查找操作比ArrayList慢,但是增删等操作由于不需要移动数据,因此会比ArrayList快。但是事实上果真如此吗?对此,我们对ArrayList和LinkedList的多种情景进行分析。
定义的全局变量如下:

public static final int initSize = 100000;
public static final int changeSize = 400000;

private static ArrayList<Integer> arrayList = new ArrayList<>(initSize+changeSize);
private static LinkedList<Integer> linkedList = new LinkedList<>();

private static final Random random = new Random();

另外还有一个初始化方法,使用之前先对List初始化:

private static void initElems() {
    arrayList.clear();
    IntStream.range(0,initSize).forEach(i -> {
        arrayList.add(i);
        linkedList.add(i);
    });
}

1.插入分析

对于一个集合,我们最常用的方式是向其中插入数据,常用的方法有头插和尾插还有随机插入三种方法,现在根据这三种方法分别进行分析。为了真实有用,我们定义了一个初始大小为10000。

1.1 尾插法

尾插法就是在列表的末尾插入一个元素,代码如下:

private static void tailInsert(ArrayList arrayList,int size) {
    long start = System.currentTimeMillis();
    IntStream.range(0,size).forEach(i -> {
        arrayList.add(i);
    });
    long end = System.currentTimeMillis();
    System.out.println("ArrayList tailInsert cost:"+(end-start)+"ms");
}

private static void tailInsert(LinkedList linkedList,int size) {
    long start = System.currentTimeMillis();
    IntStream.range(0,size).forEach(i -> {
        linkedList.add(i);
    });
    long end = System.currentTimeMillis();
    System.out.println("LinkedList tailInsert cost:"+(end-start)+"ms");
}

我们只需修改changeSize的大小,然后再对结果进行比较,测试之后结果如下(时间单位ms):

数据量 10万 20万 40万 80万 160万 320万 640万
ArrayList 5 8 8 14 29 125 3528 3209
LinkedList 4 8 11 21 266 3575 7179

可以看出,在数据量较小的时候,LinkedList还是有优势的。随着数据量的增大,LinkedList的性能几乎是ArrayList的二倍以上。

1.2 头插法

我们再修改为对一个集合头部进行插入:

private static void headInsert(ArrayList arrayList,int size) {
    long start = System.currentTimeMillis();
    IntStream.range(0,size).forEach(i -> {
        arrayList.add(0,i);
    });
    long end = System.currentTimeMillis();
    System.out.println("ArrayList headInsert cost:"+(end-start)+"ms");
}

private static void headInsert(LinkedList linkedList,int size) {
    long start = System.currentTimeMillis();
    IntStream.range(0,size).forEach(i -> {
        linkedList.add(0,i);
    });
    long end = System.currentTimeMillis();
    System.out.println("LinkedList headInsert cost:"+(end-start)+"ms");
}

我们修改changeSize的大小,然后再对结果进行比较,测试之后结果如下(时间单位ms):

数据量 10万 20万 40万 80万
ArrayList 1967 4467 12813 45214
LinkedList 6 9 14 26

可以看出,LinkedList在头部插入的时候能够充分发挥其链表的优点。性能非常高。

1.3 随机插入

实际上在使用List的过程中还有一种方式是随机插入数据。

private static void randomInsert(ArrayList arrayList,int size) {
    long start = System.currentTimeMillis();
    IntStream.range(0,size).forEach(i -> {
        arrayList.add(random.nextInt(initSize+i),i);
    });
    long end = System.currentTimeMillis();
    System.out.println("ArrayList randomInsert cost:"+(end-start)+"ms");
}

private static void randomInsert(LinkedList linkedList,int size) {
    long start = System.currentTimeMillis();
    IntStream.range(0,size).forEach(i -> {
        linkedList.add(random.nextInt(initSize+i),i);
    });
    long end = System.currentTimeMillis();
    System.out.println("LinkedList randomInsert cost:"+(end-start)+"ms");
}

测试之后结果如下(时间单位ms):

数据量 10万 20万 40万 80万
ArrayList 880 2291 6663 21644
LinkedList 30589 199670 耗时太长 耗时太长

可以看到,对于随机插入,LinkedList几乎完全不能用。ArrayList在此种情况下性能还是不错的。

2.反转

对list还有一个常用的操作就是进行反转操作。

private static void reverse(ArrayList arrayList) {
    long start = System.currentTimeMillis();
    Collections.reverse(arrayList);
    long end = System.currentTimeMillis();
    System.out.println("ArrayList reverse cost:"+(end-start)+"ms");
}


private static void reverse(LinkedList linkedList) {
    long start = System.currentTimeMillis();
    Collections.reverse(linkedList);
    long end = System.currentTimeMillis();
    System.out.println("LinkedList reverse cost:"+(end-start)+"ms");
}

我们修改initSize的大小,然后再对结果进行比较,测试之后结果如下(时间单位ms):

数据量 10万 20万 40万 80万 160万 320万 640万
ArrayList 2 5 6 10 11 12 13
LinkedList 7 9 12 15 20 44 68

可以看到,虽然两个List对反转的处理性能都还可以,但是明显可以发现还是ArrayList的性能高。

3.分析

根据上述得到的结果进行分析,不难发现,除了头部插入这一种方法之外,其他的场景LinkedList的性能都低于ArrayList。
这是因为,LinkedList虽然修改操作只用改变前后节点的指针,而ArrayList由于需要在很多情况下通过System.arraycopy操作来创建新数组来实现将后续的元素挪动。看上去可能ArrayList的性能会低。但是实际上,在LinkedList中,对需要操作的节点寻址只能扫描全部的数据。而头插法恰恰是在头部操作不需要寻址,因此将LinkedList的优势发挥出来了。其他场景都不如ArrayList。
而这个寻址的方法即使我们之前提到的node方法:

    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
    
       public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

在add方法中,如果index与size不等,则不得不用node方法找到索引位置的节点。

4.总结

经过以上分析我们可以得出,LinkedList的增删性能还真不一定比ArrayList快,在某些场景下可能还会比ArrayList低。因此,我们在使用的时候需要对其使用场景进行了解。通常情况下ArrayList的性能都好于ArrayList。
而LinkedList的头插法比ArrayList高效,这实际上也是队列的一种基本用法。实际上LinKedList本身也是一个Deque结构。
对于LinkedList,其作者也在twitter上回复:


image.png

实际上LinkedList不会有人真的用。

上一篇下一篇

猜你喜欢

热点阅读