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()的方法,其中一个是

 public void add(int indexelement) {
 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> first;
15            for (int = 0indexi++)
16                x.next;
17            return x;
18        else {
19            Node<E> last;
20            for (int size - 1indexi--)
21                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的“庐山真面目”的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode 008 链表系列

LeetCode 008 链表系列

Java数据结构及算法实战系列008:Java队列02——阻塞队列BlockingQueue

Java数据结构及算法实战系列008:Java队列02——阻塞队列BlockingQueue

java集合系列之LinkedList源码分析

Java入门系列之集合LinkedList入门