title: LinkedList原理解析
date: 2019-03-04 15:13:49
tags: Java基础
LinkedList原理解析
在前面我们对
ArrayList
(See my ArrayList原理解析 page for details.)已做了解析,ArrayList
操作维护的是内部数组,元素在内存中是连续存放的,可以通过索引直接访问,访问效率高,但是对于删除和移动来说,性能就较低了。而LinkedList
呢,顾名思义,它是一个链表,更确切的说,它是一个双向链表,因为LinkedList
的元素都是单独存放的,元素之间在逻辑上通过链接连在一起,下面,我们就解析下LinkedList
的原理。
在LinkedList
中元素以节点(Node
)的方式进行存储:
private static class Node<E> {
//代表本节点元素
E item;
//代表后继元素
Node<E> next;
//代表前驱元素
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
在
LinkedList
中有个内部类Node
,它来表示LinkedList
中的元素。而在其内部也保存着当前节点的前驱和后继两个节点。
在LinkedList
中维护着三个变量:
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
LinkedList
的长度,头节点以及尾节点。LinkedList
里的操作大都是围绕着这三个变量来进行的。我们先从add
方法看起。
public boolean add(E e) {
linkLast(e);
return true;
}
Ladd(E e)
方法,将调用linkLast(E e)
方法,插入新节点。
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
第一步:创建新节点,前驱节点指向尾节点,后继节点为空。
final Node<E> newNode = new Node<>(l, e, null);
第二步:令尾节点为这个新建的节点;若原来链表为空,则头节点指向新节点;非空,则尾节点的后继节点指向新节点;链表长度加1;modCount加1,用于迭代时判断链表的结构变化。
这是在链表末尾进行插入,若要指定位置插入呢?再看看另外一个add(int index,E element)
方法
public void add(int index, E element) {
//检查索引值是否满足index >= 0 && index <= size
checkPositionIndex(index);
//若index值与链表长度相同,则插入到链表末尾。
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
检查完毕,并且想要插入的位置与链表长度包不同,这调用linkBefore
方法进行插入。
因为链表无法向数组那样可以直接查找索引,进行插入,所以要根据索引值查找到对应的节点,在这里调用了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;
}
}
与链表长度的一半来进行比较,若index小于链表长度的一半,则从头开始;反之,则从尾节点开始。最后返回找到对应索引值的节点。
找到index对应的节点后,就可以插入了。
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
//如果index节点为头节点,则新节点为头节点;不然则插入到index节点之前
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
在中间插入,
LinkedList
只需要分配内存就行,而ArrayList
则需要其他空间,还要移动后续的元素。
我们再来看看remove(int index)
方法。
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
remove(int index)
方法同样也需要检查index值,然后找到index值对应的节点,最后调用unlink
删除节点后,返回删除的节点。
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {j
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
remove(int index)
方法的基本思路也很简单:将删除节点x
的前驱节点指向x
的后继节点,将x
的候后继节点的前驱节点指向x
的前驱节点。首先,若
x
的前驱节点为空,即x
为头节点,则x
的后继自然就是头节点了。
然后,若
x
的后继节点为空,即x
为尾节点,则x
的前驱自然就是头节点了。
结论
以上,我们介绍了LinkedList
的几个方法,其余方法也都类似,就是链表的一些基本操作。LinkedList
内部是以node节点的方式来进行维护的,每个节点内部又有前驱和后继节点,这就相当于一个双向链表,并且在内部还维护着头节点、尾节点以及长度。通过这些,可以得出一些关于LinkedList
的一些特点:
(1)LinkedList
不需要预先分配空间,按需进行分配;
(2)进行头、尾的插入很方便;
(3)按索引插入,时间复杂度较低,为O(N/2),但插入效率较高,为O(1);
(4)查找的话,效率也较低,时间复杂度为O(N),不管是否已排序;
(5)在两端进行查找、删除,时间复杂度为O(1);
(6)在中间进行查找、删除,需要逐个比对,时间复杂度为O(N),但修改效率就只有O(1)。
综上,若进行的操作涉及大量的插入、删除,尤其是在两端的插入、删除,并且查找中间元素的操作较少的话,使用LinkedList
是比较好的选择。