Java系列008和你一起揭开LinkedList的“庐山真面目”
Posted 1994jinnan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java系列008和你一起揭开LinkedList的“庐山真面目”相关的知识,希望对你有一定的参考价值。
你好!我是miniluo,又到了周末,我将和你一起推翻认知里的LinkedList,它并非如教科书所言那么的高效。
JDK集合类List,我们最常用的莫过于ArrayList。而面试官也是最常考察应聘者对ArrayList和LinkedList的理解,其实考察的是应聘者对数据结构的掌握程度。
我们知道两者代表两种不同数据结构,分别是数组和(双向)链表。数据结构通常都是用大O时间复杂度来比对,通过下图(图片来源:https://www.bigocheatsheet.com/),我们可以看到数组和链表两者之间大O的显著差异:
总结就两点:
1、对于数组,随机元素访问的时间复杂度是 O(1),元素插入操作是 O(n)。
2、对于链表,按偏移量访问的时间复杂度是 O(n),元素插入操作是 O(1)。
而我们从教科书中可以知道,链表插入速度快,适用于频繁插入,较少随机读取的场景中。那LinkedList真的如教科书所言?如我们所想吗?“实践是检验真理的唯一标准”,没有实践就下断言的结论是耍流氓,不具备科学性;秉着这个思想,我们撸一波代码验证一番。
元素访问时间复杂度
我们首先来看两者的随机访问时间复杂度,看看下文代码:
1 /**
2 * ArrayList访问
3 * @param elementCount 元素个数
4 * @param loopCount 循环次数
5 */
6 private static void arrayListGet(int elementCount, int loopCount) {
7 List<Integer> list = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(ArrayList::new));
8 IntStream.rangeClosed(1, loopCount).forEach(i -> list.get(ThreadLocalRandom.current().nextInt(elementCount)));
9 }
10 /**
11 * LinkedList访问
12 * @param elementCount 元素个数
13 * @param loopCount 循环次数
14 */
15 private static void linkedListGet(int elementCount, int loopCount) {
16 List<Integer> list = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(LinkedList::new));
17 IntStream.rangeClosed(1, loopCount).forEach(i -> list.get(ThreadLocalRandom.current().nextInt(elementCount)));
18 }
我们指定elementCount和loopCount分别都是10w,在main函数执行测试代码。
1int elementCount = 100000;
2int loopCount = 100000;
3StopWatch stopWatch = new StopWatch();
4stopWatch.start("linkedListGet");
5linkedListGet(elementCount, loopCount);
6stopWatch.stop();
7stopWatch.start("arrayListGet");
8arrayListGet(elementCount, loopCount);
9stopWatch.stop();
10System.out.println(stopWatch.prettyPrint());
执行结果:
1StopWatch ‘‘: running time (millis) = 4036
2-----------------------------------------
3ms % Task name
4-----------------------------------------
504024 100% linkedListGet
600012 000% arrayListGet
从执行结果看,确实如我对数组和链表两种数据结构的认识一样,LinkedList耗时4秒左右,而ArrayList只需0.1秒。好,接下来我们试试插入操作时间复杂度是否真的如教科书所说那样。
元素随机插入时间复杂度
我们新增下面2个方法,之后也是分别设置elementCount和loopCount都是10w。
1 /**
2 * ArrayList插入
3 * @param elementCount 元素个数
4 * @param loopCount 循环次数
5 */
6 private static void arrayListAdd(int elementCount, int loopCount) {
7 List<Integer> list = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(ArrayList::new));
8 IntStream.rangeClosed(1, loopCount).forEach(i -> list.add(ThreadLocalRandom.current().nextInt(elementCount),1));
9 }
10 /**
11 * LinkedList插入
12 * @param elementCount 元素个数
13 * @param loopCount 循环次数
14 */
15 private static void linkedListAdd(int elementCount, int loopCount) {
16 List<Integer> list = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toCollection(LinkedList::new));
17 IntStream.rangeClosed(1, loopCount).forEach(i -> list.add(ThreadLocalRandom.current().nextInt(elementCount),1));
18 }
在main函数执行测试代码
1int elementCount = 100000;
2int loopCount = 100000;
3StopWatch stopWatchAdd = new StopWatch();
4stopWatchAdd.start("linkedListAdd");
5linkedListAdd(elementCount, loopCount);
6stopWatchAdd.stop();
7stopWatchAdd.start("arrayListAdd");
8arrayListAdd(elementCount, loopCount);
9stopWatchAdd.stop();
10System.out.println(stopWatchAdd.prettyPrint());
此时……,你可以去喝几口茶再回来……
1StopWatch ‘‘: running time (millis) = 34540
2-----------------------------------------
3ms % Task name
4-----------------------------------------
532911 095% linkedListAdd
601629 005% arrayListAdd
天呐(Oh,my god),LinkedList竟然耗时32秒,ArrayList只需要1.6秒,居然让我们大跌眼镜?不是说LinkedList的插入比ArrayList快?
真相是……
我们进入LinkedList的源码,找到add()的方法,其中一个是
1 public void add(int index, E element) {
2 checkPositionIndex(index);
3
4 if (index == size)
5 linkLast(element);
6 else
7 linkBefore(element, node(index));
8 }
9
10Node<E> node(int index) {
11 // assert isElementIndex(index);
12
13 if (index < (size >> 1)) {
14 Node<E> x = first;
15 for (int i = 0; i < index; i++)
16 x = x.next;
17 return x;
18 } else {
19 Node<E> x = last;
20 for (int i = size - 1; i > index; i--)
21 x = x.prev;
22 return x;
23 }
24 }
看第7行node(index),再看node()方法的实现,也就是我们要完成随机插入,则首先需要循环遍历找到index所在节点。循环遍历,那不也是O(n)。
为何我们会认为LinkedList的插入时间复杂度是O(1)呢,这是因为如果直接在链表尾部或首部,以及知道了插入节点则其插入时间复杂度确实是O(1)。因此在实际项目中使用应该要注意这个问题,以免自认为对的实际上导致了程序执行效率低下的结果。今天的案例已同步GitHub(https://github.com/littleluo/bj-share-java.git)。
好了下面,我们总结下今天的内容。
总结
今天,和你一起写了一段代码验证ArrayList和LinkedList的随机访问和随机插入时间复杂度的结论,随机访问事实上如教科书所说那样,但是随机插入则让我们大跌眼镜。这个结论告诉我们不要凡事“想当然”,该用实践证明的不能偷懒,多看源码的实现过程,以免踩雷。另外我们测试案例用到了JDK1.8的特性IntStream,如果对此不熟悉的同学可以先去看看IntStream,有助于你读懂案例代码。
思考和讨论
1、文中关于各种数据结构的对别图,我们只举了数组和链表进行了实际比对,有兴趣的同学可以写代码看看其他数据结构是否真的如图所述呢?
2、实际项目中你有用过LinkedList吗?
欢迎留言与我分享和指正!也欢迎你把这篇文章分享给你的朋友或同事,一起交流。
感谢您的阅读,我们下节再见!
来源:站长
以上是关于Java系列008和你一起揭开LinkedList的“庐山真面目”的主要内容,如果未能解决你的问题,请参考以下文章
Java数据结构及算法实战系列008:Java队列02——阻塞队列BlockingQueue