LinkedList 源码分析
前言
有了ArrayList,自然少不了LinkedList了。
下面我就以面试问答的形式学习我们的常用的装载容器——LinkedList
(源码分析基于JDK8)
问答内容
LinkedList 用来做什么,怎么使用?
问:请简单介绍一下您所了解的LinkedList,它可以用来做什么,怎么使用?
答:
- LinkedList底层是双向链表,同时实现了List接口和Deque接口,所以它既可以看作是一个顺序容器,也可以看作是一个队列(Queue),同时也可以看作是一个栈(Stack),但如果想使用栈或队列等数据结构的话,推荐使用ArrayDeque,它作为栈或队列会比LinkedList有更好的使用性能。
示例代码:
|
|
- LinkedList底层实现是双向链表,核心组成元素有:
int size = 0
用于记录链表长度;Node<E> first;
用于记录头(第一个)结点(储存的是头结点的引用);Node<E> last;
用于记录尾(最后一个)结点(储存的是尾结点的引用)。
示例代码:
|
|
- 双向链表的核心组成元素还有一个最重要的
Node<E>
,Node<E>
包含:E item;
用于存储元素数据,Node<E> next;
指向当前元素的后继结点,Node<E> prev;
指向当前元素的前驱结点。
示例代码:
|
|
双向链表底层实现,图片来自网络
上图中的head即Node first; tail即Node last;
LinkedList 的操作和对应的时间复杂度。
问:请分别分析一下它是如何获取元素,修改元素,新增元素与删除元素,并分析这些操作对应的时间复杂度。
答:
- 获取元素:LinkedList提供了三种获取元素的方法,分别是:
- 获取第一个元素
getFirst()
,获取第一个元素,直接返回Node<E> first
指向的结点即可,所以时间复杂度为O(1)。 - 获取最后一个元素
getLast()
,获取最后一个元素,直接返回Node<E> last
指向的结点即可,所以时间复杂度也为O(1)。 - 获取指定索引index位置的元素
get(int index)
,由于Node<E>
结点在内存中存储的空间不是连续存储的,所以查找某一位置的结点,只能通过遍历链表的方式查找结点,因此LinkedList会先通过判断index < (size >> 1)
,size>>1
即为size/2
当前链表长度的一半,判断index的位置是在链表的前半部分还是后半部分。决定是从头部遍历查找数据还是从尾部遍历查找数据。最坏情况下,获取中间元素,则需要遍历n/2次才能获取到对应元素,所以此方法的时间复杂度为O(n)。
- 综上所述,LinkedList获取元素的时间复杂度为O(n)。
示例代码:
|
|
- 修改元素:LinkedList提供了一种修改元素数据的方法
set(int index, E element)
,修改元素数据的步骤是:1.检查index索引是否合法[0,size)。2.折半查询获取对应索引元素。3.将新元素赋值,返回旧元素。由获取元素的分析可知,折半查询的时间复杂度为O(n),故修改元素数据的时间复杂度为O(n)。
示例代码:
|
|
- 新增元素:LinkedList提供了四种新增元素的方法,分别是:
- 将指定元素插入到链表的第一个位置中
addFirst(E e)
,只需将头结点first
指向新元素结点,将原第一结点的前驱指针指向新元素结点即可。不需要移动原数据存储位置,只需交换一下相关结点的指针域信息即可。所以时间复杂度为O(1)。 - 将指定元素插入到链表的最后一个位置中
addLast(E e)
,只需将尾结点last
指向新元素结点,将原最后一个结点的后继指针指向新元素结点即可。不需要移动原数据存储位置,只需交换一下相关结点的指针域信息即可。所以时间复杂度也为O(1)。 - 添加元素方法
add(E e)
等价于addLast(E e)
。 - 将指定元素插入到链表的指定位置index中
add(int index, E element)
,需要先根据位置index调用node(index)
遍历链表获取该位置的原结点,然后将新结点插入至原该位置结点的前面,不需要移动原数据存储位置,只需交换一下相关结点的指针域信息即可。所以时间复杂度也为O(1)。
- 综上所述,LinkedList新增元素的时间复杂度为O(1),单纯论插入新元素,操作是非常高效的,特别是插入至头部或插入到尾部。但如果是通过索引index的方式插入,插入的位置越靠近链表中间所费时间越长,因为需要对链表进行遍历查找。
添加元素结点示意图,图片来自《大话数据结构》
|
|
- 删除元素:LinkedList提供了四种删除元素的方法,分别是:
- 删除链表中的第一个元素
removeFirst()
,只需将头结点first
指向删除元素结点的后继结点并将其前驱结点指针信息prev
清空即可。不需要移动原数据存储位置,只需操作相关结点的指针域信息即可。所以时间复杂度为O(1)。 - 删除链表中的最后一个元素
removeLast()
,只需将尾结点last
指向删除元素结点的前驱结点并将其后继结点指针信息next
清空即可。不需要移动原数据存储位置,只需操作相关结点的指针域信息即可,所以时间复杂度也为O(1)。 - 将指定位置index的元素删除
remove(int index)
,需要先根据位置index调用node(index)
遍历链表获取该位置的原结点,然后将删除元素结点的前驱结点的next
后继结点指针域指向删除元素结点的后继结点node.prev.next = node.next
,删除元素结点的后继结点的prev
前驱结点指针域指向删除元素结点的前驱结点即可node.next.prev = node.prev
(此处可能有些绕,不太理解的同学自行学习一下双向链表的数据结构吧),不需要移动原数据存储位置,只需交换一下相关结点的指针域信息即可。所以时间复杂度也为O(1)。
删除元素结点示意图,图片来自《大话数据结构》
- 删除传入的Object o指定对象,比较对象是否一致通过o.equals方法比较
remove(Object o)
,和3.的思路基本差不多,关键是比较对象是通过o.equals方法,记住这点即可。
- 综上所述,LinkedList删除元素的时间复杂度为O(1),单纯论删除元素,操作是非常高效的,特别是删除第一个结点或删除最后一个结点。但如果是通过索引index的方式或者object对象的方式删除,则需要对链表进行遍历查找对应index索引的对象或者利用equals方法判断对象。
示例代码:
|
|
ArrayList和LinkedList 的区别
问:那您可以比较一下ArrayList和LinkedList吗?
答:
- LinkedList内部存储的是
Node<E>
,不仅要维护数据域,还要维护prev
和next
,如果LinkedList中的结点特别多,则LinkedList比ArrayList更占内存。 - 插入删除操作效率:
LinkedList在做插入和删除操作时,插入或删除头部或尾部时是高效的,操作越靠近中间位置的元素时,需要遍历查找,速度相对慢一些,如果在数据量较大时,每次插入或删除时遍历查找比较费时。所以LinkedList插入与删除,慢在遍历查找,快在只需要更改相关结点的引用地址。
ArrayList在做插入和删除操作时,插入或删除尾部时也一样是高效的,操作其他位置,则需要批量移动元素,所以ArrayList插入与删除,快在遍历查找,慢在需要批量移动元素。 - 循环遍历效率:
- 由于ArrayList实现了
RandomAccess
随机访问接口,所以使用for(int i = 0; i < size; i++)遍历会比使用Iterator迭代器来遍历快:
|
|
- 而由于LinkedList未实现
RandomAccess
接口,所以推荐使用Iterator迭代器来遍历数据。 - 因此,如果我们需要频繁在列表的中部改变插入或删除元素时,建议使用LinkedList,否则,建议使用ArrayList,因为ArrayList遍历查找元素较快,并且只需存储元素的数据域,不需要额外记录其他数据的位置信息,可以节省内存空间。
LinkedList是线程安全的吗?
问:LinkedList是线程安全的吗?
答:LinkedList不是线程安全的,如果多个线程同时对同一个LinkedList更改数据的话,会导致数据不一致或者数据污染。如果出现线程不安全的操作时,LinkedList会尽可能的抛出ConcurrentModificationException
防止数据异常,当我们在对一个LinkedList进行遍历时,在遍历期间,我们是不能对LinkedList进行添加,删除等更改数据结构的操作的,否则也会抛出ConcurrentModificationException
异常,此为fail-fast(快速失败)机制。从源码上分析,我们在add,remove
等更改LinkedList数据时,都会导致modCount的改变,当expectedModCount != modCount
时,则抛出ConcurrentModificationException
。如果想要线程安全,可以考虑调用Collections.synchronizedCollection(Collection<T> c)
方法。
示例代码:
|
|
总结
LinkedList的结论已在第三个问题中展现了一部分了,所以不再重复说明了,我以面试问答的形式和大家一同学习了LinkedList,由于没有时间画图,可能此次没有ArrayList说的那么清楚,如果大家有看不懂的地方,请自行看一下关于链表的数据结构吧。如果此文对你有帮助,麻烦点个喜欢,谢谢各位。