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 可能破坏其内部结构。

LinkedList 的 API 与数据结构的更多相关文章

  1. 采用LinkedList来模拟栈数据结构的集合--先进后出

    三.用LinkedList来模拟栈数据结构的集合 /* * 自定义一个数据结构为LinkedList的集合类*/public class MyCollection_LinkedList { publi ...

  2. Java面向对象_常用类库api——二叉树数据结构实现

    二叉树是每个节点最多有两个子树的有序树.通常子树被称为"左子树"和"右子树". 二叉树算法的排序规则: 1.选择第一个元素作为根节点 2.之后如果元素大于根节点 ...

  3. LinkedList学习:API调用、栈、队列实现

    参考的博客 Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例 如果你想详细的区了解容器知识以及本文讲的LinkedList,我推荐你去看这篇博客和这个做个的容器系列 Lin ...

  4. 集合中list、ArrayList、LinkedList、Vector的区别、Collection接口的共性方法以及数据结构的总结

    List (链表|线性表) 特点: 接口,可存放重复元素,元素存取是有序的,允许在指定位置插入元素,并通过索引来访问元素 1.创建一个用指定可视行数初始化的新滚动列表.默认情况下,不允许进行多项选择. ...

  5. 数据结构与算法分析java——线性表3 (LinkedList)

    1. LinkedList简介 LinkedList 是一个继承于AbstractSequentialList的双向链表.它也可以被当作堆栈.队列或双端队列进行操作.LinkedList 实现 Lis ...

  6. 每周一练 之 数据结构与算法(LinkedList)

    这是第三周的练习题,原本应该先发第二周的,因为周末的时候,我的母亲大人来看望她的宝贝儿子,哈哈,我得带她看看厦门这座美丽的城市呀. 这两天我抓紧整理下第二周的题目和答案,下面我把之前的也列出来: 1. ...

  7. Java 集合系列05之 LinkedList详细介绍(源码解析)和使用示例

    概要  前面,我们已经学习了ArrayList,并了解了fail-fast机制.这一章我们接着学习List的实现类——LinkedList.和学习ArrayList一样,接下来呢,我们先对Linked ...

  8. Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例

    java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...

  9. java集合系列——List集合之LinkedList介绍(三)

    1. LinkedList的简介 JDK 1.7 LinkedList是基于链表实现的,从源码可以看出是一个双向链表.除了当做链表使用外,它也可以被当作堆栈.队列或双端队列进行操作.不是线程安全的,继 ...

随机推荐

  1. [从源码学设计]蚂蚁金服SOFARegistry之存储结构

    [从源码学设计]蚂蚁金服SOFARegistry之存储结构 目录 [从源码学设计]蚂蚁金服SOFARegistry之存储结构 0x00 摘要 0x01 业务范畴 1.1 缓存 1.2 DataServ ...

  2. (数据科学学习手札100)搞定matplotlib中的字体设置

    本文示例文件已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 matplotlib作为数据可视化的利器,被广泛 ...

  3. 时间转成x时x分x秒的封装(简易版)

    function createTime(t) { let timer; if (t <= 0 || !t || t < 60 || typeof(t)!=='number') timer ...

  4. 一、less命令查看日志

    查看日志时,一般用less满足大部分的需求. 使用命令格式: less [要查看的文件名] 例如:less LOG.20201211 中间加参数命令格式 less 参数 [要查看的文件名] 例如:查看 ...

  5. .NET Core集成CorrelationId实现全链路日志输出

    .NET Core集成CorrelationId实现全链路日志输出 一,链路追踪 随着微服务架构的流行,一次请求会涉及多个服务的调用,并且服务本身也可能会依赖其他服务,整个请求路径会构成一个调用链,当 ...

  6. 冰河,能不能讲讲如何实现MySQL数据存储的无限扩容?

    写在前面 随着互联网的高速发展,企业中沉淀的数据也越来越多,这就对数据存储层的扩展性要求越来越高.当今互联网企业中,大部分企业使用的是MySQL来存储关系型数据.如何实现MySQL数据存储层的高度可扩 ...

  7. 官宣 | Apache Flink 1.12.0 正式发布,流批一体真正统一运行!

    官宣 | Apache Flink 1.12.0 正式发布,流批一体真正统一运行! 原创 Apache 博客 [Flink 中文社区](javascript:void(0) 翻译 | 付典 Revie ...

  8. 网站开发学习Python实现-Django的models学习-生鲜项目(6.3.2)

    @ 目录 1.说明 2.模型类的设计 3.代码的具体实现 4.详情地址 关于作者 1.说明 models是django的很重要的部分,所以深入研究. 本文章的所研究项目为黑马教育python课程中的项 ...

  9. 浅析JavaWeb开发模式:Model1和Model2

    一.前言 在学习JavaWeb的过程中,大家都会接触到Model1和Model2,历史的发展过程是Model1 → Model2.那么它们之间有何相同之处和不同之处呢? 二.Model1 Model1 ...

  10. 302跳转导致的url劫持

    介绍一个   网站监测工具:iis7网站监测IIS7网站监控工具可以做到提前预防各类网站劫持,并且是免费在线查询,适用于各大站长,政府网站,学校,公司,医院等网站.它可以做到24小时定时监控,同时它可 ...