LinkedList 是 List 接口和 Deque 接口的双向链表实现,它所有的 API 调用都是基于对双向链表的操作。本文将介绍 LinkedList 的数据结构和分析 API 中的算法。

数据结构

LinkedList 的数据结构是一个双向链表,它有两个成员变量:first 和 last,分别指向双向队列的头和尾。

.st1 {fill:#191919;font-family:Times New Roman;font-size:9pt}
prevnext'A'prevnext'B'prevnext'C'firstlast

Node<E> first;
Node<E> last;

这里“双向”的含义是相对单链表而言的,双向链表的节点不仅有后继,还有前驱。LinkedList 中双向链表的节点是一个个的 Node,它是 LinkedList 的一个静态内部类。其定义如下。Node 是一个泛型类,泛型参数是存放在 LinkedList 中的值的类型。

    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 如下图所示。

LinkedList 的 API 都是基于双端队列的操作来实现的,这些操作被封装成了一系列的 private 方法。下面对这些私有方法进行分析。

插入操作:linkFirst(e) 与 linkLast(e)

LinkedList 通过 linkFirst(e) 与 linkLast(e) 分别往双向链表的头部和尾部插入元素。插入元素时需要考虑两种情况:1)双向链表中不包含元素;2)双向链表中已经包含了元素。插入元素属于修改操作,因此操作数 modCount 需要进行自增。更多关于 modCount 的说明可以参考这篇:(ArrayList 源码分析)[https://www.cnblogs.com/robothy/p/13969448.html]。linkFirst(e) 源码如下,linkLast(e) 源码与前者类似。

    private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f); // 创建一个 Node 对象 e,在构造方法中,e 的后继已经指向了旧的 first。
first = newNode;
if (f == null) // 处理边界情况:双向链表中没有元素
last = newNode;
else
f.prev = newNode;
size++; // size 用于统计 LinkedList 中的元素个数
modCount++; // 统计操作数,用于支持迭代时 fail-fast 机制
}

在指定元素前面插入:linkBefore(e, succ)

linkBefore(e, succ) 在元素 succ 前面插入元素 e,它需要考虑两种情况:1)succ 的前驱为空;2)succ 的前驱不为空。在指定元素前面插入操作时间复杂度为 O(1),相对 ArrayList 时间复杂度为 O(n) 插入来说,效率极高。(n 为 List 中已有元素的个数)

    void linkBefore(E e, Node<E> succ) {
// 这里没有对 succ 为空进行检查,因为是 private 方法,在外层确保输入不为空即可
final Node<E> pred = succ.prev; // 获取 succ 的前驱 final Node<E> newNode = new Node<>(pred, e, succ); // 构造一个新的节点,此时新节点的前驱指向 succ 的前驱,新节点的后继指向 succ
succ.prev = newNode; // 更新 succ 的前驱指向
if (pred == null) // succ 前驱原来所指元素为空的情况
first = newNode; // 更新 first 指针
else
pred.next = newNode; // 否则更新 succ 原来前驱所指即可
size++;
modCount++;
}

移除头部和尾部元素:unlinkFirst(f) 和 unlinkLast(l)

unlinkFirst(f) 操作将移除头部节点,它需要考虑两种情况:1)链表中只有 1 个元素;2)链表中有超过 1 个元素。

    private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null; // 这两个 null 赋值操作斩断了引用链,让 GC 能够回收对象。
f.next = null; // help GC
first = next;
if (next == null) // 只有 1 个元素的情况,last, first 指向同一个元素,因此移除了 first 所指向的元素之后,last 也要更新
last = null;
else // 含有多个元素的情况
next.prev = null;
size--;
modCount++;
return element;
}

移除指定元素:unlink(x)

unlink(x) 在移除指定元素时也是小心翼翼。这个方法在功能上可以替代 unlinkFirst(f) 和 unlinkLast(f),不过因为 LinkedList 对头和尾的操作及其频繁,因此用单独的更高效的函数进行处理可以在一定程度上提升性能。

    E unlink(Node<E> x) {
// assert x != null;
final E element = x.item; // 取出要返回的值
// 拿到 x 的前驱和后继
final Node<E> next = x.next;
final Node<E> prev = x.prev; // 处理前驱指针
if (prev == null) { // 前驱所指为空,表示 x 为头部元素
first = next;
} else { // 前驱不为空
prev.next = next;
x.prev = null; // 帮助 GC
} // 处理后继指针
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null; // 帮助 GC
} x.item = null; // 帮助 GC
size--;
modCount++;
return element;
}

根据索引获取指定节点: node(index)

因为是链表结构,要根据位置获取节点只能以迭代的方式进行,时间复杂度为 O(n),这里的 node(index) 方法做了一点优化:若索引号 index 在前半部分,则从头节点开始遍历;若索引好 index 在后半部分,则从尾节点开始遍历。

    Node<E> node(int index) {
// assert isElementIndex(index); if (index < (size >> 1)) { // 如果 index 小于 size 的一半,则从头部开始遍历
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else { // 如果 index 大于等于 size 的一半,则从尾部开始遍历
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

以上部分就是 LinkedList 中双端队列的操作了,不过这些方法都被 private 修饰,因此开发人员无法直接调用它们,不过 LinkedList 所暴露出来的 API 几乎都是调用这些 private 方法来完成操作的。下面介绍 LinkedList 的相关 API。

构造方法

LinkedList 的构造方法有两个,一个是无参构造方法,另一个构造方法可以传入一个集合。

  • LinkedList() :构造一个空的列表;
  • LinkedList(Collection<? extends E> c) :构造一个列表,并将集合中的元素插入到列表中,插入顺序与集合的迭代器返回元素的顺序一致。

LinkedList 作为 List 接口的实现类

size()

LinkedList 内部维护了一个成员变量 size,每次插入或者删除元素时都会更新该变量的值。size() 方法仅仅是返回了该变量的值。

isEmpty()

通过 size 的值来判断,size 为 0 即表示 LinkedList 为空。

indexOf(o)

indexOf(o) 将返回指定元素 o 在 LinkedList 中首次出现的位置(头节点到尾节点方向),它需要从头节点开始遍历双向链表。如果元素不存在,则返回 -1。传入的 o 可以为 null,源码中专门分了两个分支来处理传入的 o 为 null 和非 null 的问题。时间复杂度为 O(n),其中 n 为 LinkedList 中元素的数量。

    public int indexOf(Object o) {
int index = 0;
if (o == null) { // 处理 o 为 null 的情况
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
} else { // 处理 o 不为 null 的情况
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
return -1;
}

lastIndexOf(o)

lastIndexOf(o) 与 indexOf(o) 相反,它从双向链表的尾部开始遍历,返回元素 o 在 LinkedList 中最后出现的位置。返回 -1 表示不包含元素 o。

contains(o)

contains(o) 方法调用了 indexOf(o),通过检查返回值是否为 -1 来判断 LinkedList 中是否包含了 o。

add(e)

add(e) 将元素 e 添加到双向链表的末尾,此方法直接调用了 linkLast(e) 方法完成了操作。

    public boolean add(E e) {
linkLast(e);
return true;
}

add(index, element)

此方法将元素添加到指定的索引位置,它分了两种情况:一种是 index == size,直接添加到双向链表尾部即可;另一种是非添加到尾部,需要先迭代找到索引位置为 index 的元素,然后将新元素插入到它前面。时间复杂度为 O(n)。

    public void add(int index, E element) {
checkPositionIndex(index); // 暴露给用户的 API,需要对用户的输入进行检查 if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

set(index, element)

此方法调用了 node(index) 获取 element 所在的 Node,然后将 element 挂到了 Node 上。时间复杂度为 O(n)。

    public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}

remove(index)

remove(index) 移除索引为 index 的元素,先根据索引获取节点,然后调用 unlink(e) 移除节点。

    public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}

remove(o)

remove(o) 将查找元素 o 在 LinkedList 中第一次所在的节点,然后移除该节点。这一操作需要遍历双向链表。需要注意的是 remove(o) 并不会移除所有的 o ,只会移除第 1 个。

    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;
}

listIterator()

LinkedList 没有单独的内部类实现 Iterator 接口,调用 iterator() 方法返回的本质是一个 ListItr,和 listIterator() 返回的是一样的迭代器。

ListItr 允许从指定下标位置开始迭代,下标位置通过构造方法的参数传入。

        ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}

LinkedList 有两个获取 ListItr 的 API,分别是 listIterator() 与 listIterator(index),二者本质一样。

    public ListIterator<E> listIterator() {
return listIterator(0);
} public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}

ListItr 在迭代的过程中 LinkedList 不能够被其它的线程改变,否则可能抛出 ConcurrentModificationException。这是一种 fail-fast 策略,通过修改数 modCount 来实现,前面可以看到,凡是会改变链表结构的操作都会更新 modCount 的值。在迭代的过程中不断检查 modCount 是否和期望的值一致,如果不一致,则说明有其它的线程修改了双向链表的结构。此时 LinkedList 中的数据可能出现错误,但如果没有 fail-fast 机制,这种错误可能不会立即暴露出来,系统可能需要运行很长时间才暴露,到那时可能已经产生严重后果了,后面再来排查错误原因也及其困难。

通过 modCount 机制来探测这类难以错误,一旦探测到,立即报告,这就是 fail-fast 机制。不过由于多线程操作本身存在着不确定性,modCount 也并非一定能够探测到这种错误。为了避免这种错误,在多线程访问同一个 LinkedList 对象时应该进行线程同步,最好就时不让多线程访问同一个 LinkedList。

不过 ListItr 允许迭代器自身修改 LinkedList,它在修改之后会更新 modCount,支持的修改操作包括:

  • remove() 移除刚刚返回的元素
  • set(e) 将刚刚返回的元素所在 Node 节点的值修改为 e
  • add(e) 在刚刚返回的元素后面插入 e

LinkedList 作为 Deque 接口的实现类

双端队列接口 Deque 提供了一组在线性集合头部和尾部进行操作的 API,LinkedList 在通过操作双端队列的头部和尾部实现这些抽象方法。

新增头(尾)部元素:addFirst(e), addLast(e), offer(e), offerFirst(e), offerLast(e), push(e)

    public void addFirst(E e) {
linkFirst(e); // LinkedList 支持存放 null
}

获取头(尾)部元素:getFirst(), getLast(), peek(), peekFirst(), peekLast()

    public E getFirst() {
final Node<E> f = first;
if (f == null) // getXXX() 抛出遗产,peekXXX() 使用特殊值 null 来表示没有元素
throw new NoSuchElementException();
return f.item;
}

删除头(尾)部元素:removeFirst(), removeLast(), poll(), pollFirst(), pollLast(), pop()

    public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}

小结

LinkedList 内部是双向链表结构,新增,删除元素很方便,支持存放 null 元素。

LinkedList 实现了 List 和 Deque 接口。作为 List,LinkedList 适用于数量未知且需要大量增删操作情形,若需要随机访问或者大量查询,应该使用 ArrayList;作为 Deque,LinkedList 适用于容量未知的情形,如果容量已知,则使用 ArrayDeque 效率会更高一些。

LinkedList 是非线程安全的,多个线程同时访问一个 LinkedList 可能破坏其内部结构。

最新文章

  1. C语言编程风格(转发)
  2. xcode6.0 模拟器打不开
  3. ubuntu14.04编译安装Git2.7
  4. sshd调优
  5. Win7_关闭休眠文件hiberfil.sys
  6. C语言中的三字母词
  7. ListView 文件重命名
  8. bzoj1681[Usaco2005 Mar]Checking an Alibi 不在场的证明
  9. kafka 使用、介绍
  10. [编织消息框架][netty源码分析]2 eventLoop
  11. MVC 常用扩展点:过滤器、模型绑定等
  12. OpenCV3.1.0中调用MHI(Motion History Images, 运动历史图像)
  13. Asp.Net Form表单控件的回车默认事件
  14. 【Python学习】Python3 环境搭建
  15. 058、flannel概述(2019-03-27 周三)
  16. pacbio bax.h5文件处理及ccs计算
  17. ASCX呼叫ASPX.CS的方法
  18. js转义和反转义html htmlencode htmldecode
  19. docker “no space left on device”问题定位解决
  20. UE4 引擎基础类说明

热门文章

  1. MyBatis if 标签的坑,居然被我踩到了。。。
  2. angular 双向数据绑定与vue数据的双向数据绑定
  3. 学习笔记:Splay
  4. 题解 CF1062E Company
  5. DarkMode(1):产品应用深色模式分析
  6. Java 设计模式 —— 组合模式
  7. 【Go语言绘图】图片的旋转
  8. 2020软件测试工程师面试题汇总(内含答案)-看完BATJ面试官对你竖起大拇指!
  9. nodeJS中的事件机制
  10. 记一次真实的webpack优化经历