你不知道的LinkedList(二):LinkedList的增删操作真的会比ArrayList快吗

时间:2022-07-23
本文章向大家介绍你不知道的LinkedList(二):LinkedList的增删操作真的会比ArrayList快吗,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

十年前,在刚解除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

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上回复:

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