剖析 LinkedList
剖析 LinkedList
SunLinkedList
是 Java 中 java.util
包提供的一个类,它实现了 List
接口,并且提供了双向链表的结构,维护了长度、头节点和尾节点,此外,还实现了
Deque
和 Queue
接口,可以按照队列、栈和双端队列的方式进行操作
LinkedList
的特点如下:
- 按需分配空间,不需要预先分配很多空间。
- 不可以随机访问,按照索引访问效率比较低,必须从头或尾顺着链表查找,时间复杂度为
。 - 不管列表是否已排序,只要是按照内容查找元素,效率都比较低,必须逐个比较,时间复杂度为
。 - 在两端添加、删除元素的效率很高,时间复杂度为
。 - 在中间插入、删除元素,要先定位,效率比较低,时间复杂度为
,但修改本身的效率很高,时间复杂度为
用法
LinkedList
的构造方法与 ArrayList
类似,有两个:一个是默认构造方法,另外一个可以接受一个已有的
Collection
,如下所示:
1 | public LinkedList(); |
可以这样创建 LinkedList
1 | List<Integer> list1 = new LinkedList<>(); |
LinkedList
与 ArrayList
一样,同样实现了
List
接口,而 List
接口扩展了
Collection
接口,Collection
又扩展了
Iterable
接口,所有这些接口的方法都是可以使用的。
队列
此外,LinkedList
还实现了队列 Queue
接口,支持先进先出,在尾部添加元素,从头部删除元素,Queue
接口的定义为:
1 | public interface Queue<E> extends Collection<E> { |
Queue
接口继承自 Collection
接口,主要操作有三类:
在尾部添加元素
add
方法,队列为满时会抛出IllegalStateException
异常offer
方法,队列为满时只是返回false
返回头部元素,但不改变队列
element
方法,队列为空时抛出NoSuchElementException
异常peek
方法,队列为空时返回null
返回头部元素,并且从队列中删除
remove
方法,队列为空时抛出NoSuchElementException
异常poll
方法,队列为空时返回null
把 LinkedList
当作队列使用:
1 | Queue<Integer> queue = new LinkedList<>(); |
栈
Java 中没有单独的栈接口,栈相关方法放在了表示双端队列的接口
Deque
中,Deque
接口提供了很多方法,栈相关的主要有三个:
1 | void push(E e); |
push
方法表示入栈,如果栈满会抛出IllegalStateException
异常pop
方法表示出栈,如果栈空会抛出NoSuchElementException
异常peek
方法查看栈顶元素,如果栈为空返回null
把 LinkList
当栈使用:
1 | Deque<Integer> stack = new ArrayDeque<>(); |
双端队列
还一个更为通用的操作两端的接口 Deque
,Deque
扩展了
Queue
,除了栈的操作方法,还有如下更为明确的操作两端的方法:
1 | public interface Deque<E> extends Queue<E> { |
xxxFirst
方法操作头部,xxxLast
方法操作尾部,每种操作有两种形式:
- 队列为空时
getXXX/removeXXX
会抛出异常,而peekXXX/pollXXX
会返回null
; - 队满时
addXXX
会抛出异常,offerXXXX
只是返回false。
Deque
接口还有个迭代器方法,可以从后往前遍历
1 | Iterator<E> descendingIterator(); |
例如
1 | public class Main { |
输出
1 | 10987654321 |
实现原理
LinkedList 的内部组成
LinkedList
的内部实现是双向链表,每个元素在内存中单独存放,通过链接连在一起
为了表示链接的关系,需要一个节点的概念,节点内部包括实际存放的元素和两个指针,分别指向前一个节点(前驱)和后一个节点(后继)。节点是
LinkedList
的一个静态内部类:
1 | private static class Node<E> { |
Node
类表示节点,item
指向实际的元素,next
指向后一个节点,prev
指向前一个节点
LinkedList 内部组成就是如下三个实例变量:
1 | transient int size = 0; |
size
表示链表长度,默认为
0
,first
指向链表的头节点,last
指向链表的尾节点,初始值都为 null
构造函数
LinkedList
有两个构造函数
1 | public LinkedList(); |
无参构造函数是空的,实例变量全使用默认值
1 | public LinkedList() { |
另外一个构造函数调用 addAll
方法将传入的集合元素加入到链表中
1 | public LinkedList(Collection<? extends E> c) { |
addAll
方法调用了重载的
addAll(int index, Collection<? extends E> c)
将参数
size
作为插入位置的索引
1 | public boolean addAll(Collection<? extends E> c) { |
该方法首先调用 checkPositionIndex
方法检查
index >=0 && index <= size
,如果不是会抛出
IndexOutOfBoundsException
异常
然后将传入的集合转换成 Object
数组,计算数组的长度,如果数组长度为 0 说明没有元素可以插入,返回
false
声明两个指针,pred
指向插入位置的前一个节点,succ
指向插入位置。
如果 index == size
说明插入位置在链表末尾,succ=null
,pred
指向链表的尾部 last
否则需要调用 node()
方法查找插入位置的节点,node
方法中会先判断
index
是在链表的前半部分还是后半部分,然后决定是从前到后遍历还是从后往前遍历去查找节点。找到
succ
节点后,pred = succ.prev
然后遍历数组,创建新节点,将 pred
作为新节点的前驱,如果前驱为
null
,则说明链表为空,新节点就是头节点,将
first
指向新节点,否则
pred.next = newNode
,再下一轮循环中 pred
为
newNode
,会给 pred.next
赋值,所以创建新节点的时候,newNode.next
的传参为
null
插入完成后如果 succ
为 null
说明插入之前链表为空,尾节点就是插入的最后一个节点,所以
last = pred
,否则需要将 pred
的后继指向
succ
,succ
的前驱指向 pred
最后修改 size
和 modCount
的值
1 | public boolean addAll(int index, Collection<? extends E> c) { |
添加元素 add
add
方法用于向链表中添加元素,有两个重载
第一个 add(E e)
是向链表尾部插入元素,主要调用的是
linkLast(e)
方法
1 | public boolean add(E e) { |
linkLast
方法首先创建一个新节点
newNode
。l
和 last
指向原来的尾节点,如果原来链表为空,则为 null
。
然后修改尾节点 last
指向新的节点
newNode
,并修改前驱节点的后向指针,如果原来链表为空,则让头节点指向新节点,否则让前一个节点的next指向新节点
最后修改 size
和 modCount
的值
1 | void linkLast(E e) { |
add(int index, E element)
方法则是在链表指定位置插入元素,首先检查插入位置的合法性
然后如果 index == size
说明是在链表尾部插入,直接调用
linkLast(element);
方法
否则调用 linkBefore(element, node(index))
方法,并调用
node
方法获取插入位置的节点
1 | public void add(int index, E element) { |
linkBefore
方法的实现与 addAll
中大同小异
1 | void linkBefore(E e, Node<E> succ) { |
根据索引访问元素 get
LinkedList
提供了 get(int index)
方法根据索引访问元素
1 | public E get(int index) { |
首先调用 checkElementIndex
方法检查索引位置的有效性
然后调用 node
方法返回索引位置的节点,获取节点的
item
值即可
1 | private void checkElementIndex(int index) { |
根据内容查找元素 indexOf
LinkedList
提供了 indexOf(Object o)
方法根据内容查找元素,代码也很简单,从头节点顺着链接后找,如果要找的是
null
,则找第一个 item
为 null
的节点,否则使用 equals
方法进行比较。
1 | public int indexOf(Object o) { |
删除元素 remove
remove(int index)
方法用于删除指定位置的元素,通过
node
方法找到节点后,调用了 unlink
方法
1 | public E remove(int index) { |
unlink
方法的代码如下:
1 | E unlink(Node<E> x) { |
删除x节点,基本思路就是让x的前驱和后继直接链接起来,next是x的后继,prev是x的前驱,具体分为两步:
1)让x的前驱的后继指向x的后继。如果x没有前驱,说明删除的是头节点,则修改头节点指向x的后继。
2)让x的后继的前驱指向x的前驱。如果x没有后继,说明删除的是尾节点,则修改尾节点指向x的前驱。