Java 集合—— LinkedList
介绍
从名字上可以看出,它与 ArrayList 一样,都是 List 的具体实现子类,但它是基于链表的,链表这种数据结构相信只要学过编程或者计算机的,都应该熟悉这种数据结构,那么接下来,我将从 LinkedList 源码的角度来解读 LinkedList 对象是如何实现的。
LinkedList 定义
以下为 LinkedList 的部分定义,它保存了链表的头和尾节点,并且它是一个双向链表,list 的基本操作都是通过对链表进行操作来实现的,现在我们来具体分析基本操作的过程。
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable,
java.io.Serializable {
transient Node<E> first;
transient Node<E> last;
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;
}
}
}
基本操作
add 操作
调用 add(index,element); 方法,实际调用流程:
1. 首先会先判断当前插入的 index 位置是否合法(即是否大于当前元素个数),若不合法,则会抛出位置越界异常;否则执行 2
2. 若下标合法,则判断当前 index 是否为末尾,若是,直接调用 linkLast 方法在表尾插入元素,然后结束;否则执行 3
3. 插入的位置在链表中间,则它会找到插入位置的节点(这里为了提高查询速度,在查询时,先判断 index 在前半段还是在后半段,若在前半段则从 first 开始找;反之从 last 开始回退找),然后将该元素插入该位置,修改对应指针指向。
从上述过程可以看出,如果在表尾插入元素,则时间复杂度为 O(1);如果是表的其他位置插入元素,则时间复杂度为 O(n),因为要遍历链表,最优情况只比较一个元素,最坏情况需要比较 n/2 个元素,因此时间复杂度为 O(n)。
以下为源码实现:
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
// 插入到表尾
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++;
}
// 插入到 succ 节点前面
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;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
// 获取 index 位置的节点
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;
}
}
remove 操作
remove 操作有两种,一种是 remove(object),一种是 remove(index),前者是删除表里面第一个与 object 相等的节点元素,后者是删除 index 位置节点元素
remove(object)
该方式从 first 开始遍历链表,找到第一个与 obejct 相等的元素,找到则直接将该元素从链表上移除。但需要分两种情况,一种情况是 object 为 null 的情况,此时用 == 运算符来比较;另一种是 object 不为 null 的情况,此时用 equals 方法来比较。
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
remove(index)
该种方式先判断 index 位置是否合法,合法则直接将改位置的元素移出链表(修改前后节点的链表指向即可)
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
Iterator
LinkedList 的 迭代器与 ArrayList 一样,都是属于 fast-fail 类型,当在遍历过程时,别的线程修改了集合元素,则会抛出 ConcurrentModificationException。
总结
LinkedList,与 ArrayList 和 Vector 一样,都是 List 的实现子类,前者是基于链表实现,而后者是基于动态数组实现,因此不需要像数组那样考虑扩容操作,因此适合于元素个数不确定的情况。
在 LinkedList 中,它采用双向链表的形式,并且保存了表头和表尾节点。对于插入操作,如果在表尾或在表头插入元素,则时间复杂度为 O(1),若在表中间插入元素,则需要遍历链表找到对应位置的节点再插入元素,此时时间复杂度为 O(n)。
在遍历寻找元素的过程,由于是双向链表并且保存了头和尾节点,因此为了减少比较次数,先通过判断 index 位于前半段还是后半段,前半段则从 first 开始查找,后半段则从 last 开始查找。
另外 LinkedList 和 ArrayList 都是线程不安全,而 Vector 是线程安全;它们 Iterator 迭代器都属于 fast-fail 类型,若别的线程对集合进行修改(删除、插入),则会导致抛出 ConcurrentModificationException,要想使这两个集合变成线程安全,则可以Collections.synchronizedList(list);