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. 在.Net中所有可序列化的类都被标记为_

    [Serializable] 这个叫Attribute写在类.属性.字段的上面,比如 [Serializable] class A { ... }

  2. python自带缓存lru_cache用法及扩展(详细)

    ​ 本篇博客将结合python官方文档和源码详细讲述lru_cache缓存方法是怎么实现, 它与redis缓存的区别是什么, 在使用时碰上functiontools.wrap装饰器时会发生怎样的变化, ...

  3. hugegraph 数据存取数据解析

    hugegraph 是百度开源的图数据库,支持hbase,mysql,rocksdb等作为存储后端.本文以EDGE 存储,hbase为存储后端,来探索是如何hugegraph是如何存取数据的. 存数据 ...

  4. 最简 Spring IOC 容器源码分析

    前言 BeanDefinition BeanFactory 简介 Web 容器启动过程 bean 的加载 FactoryBean 循环依赖 bean 生命周期 公众号 前言 许多文章都是分析的 xml ...

  5. eclipse 搭建连接 activemq

    今天我特地写下笔记,希望可以完全掌握这个东西,也希望可以帮助到任何想对学习这个东西的同学. 1.下载activemq压缩包,并解压(如果需要下载请看文章尾部附录) 2.进入bin文件夹,(64位电脑就 ...

  6. tensorflow学习笔记——DenseNet

    完整代码及其数据,请移步小编的GitHub地址 传送门:请点击我 如果点击有误:https://github.com/LeBron-Jian/DeepLearningNote 这里结合网络的资料和De ...

  7. uniapp导入导出Excel

    众所周知,uniapp作为跨端利器,有诸多限制,其中之一便是vue页面不支持传统网页的dom.bom.blob等对象. 所以,百度上那些所谓的导入导出excel的方法,大部分都用不了,比如xlsx那个 ...

  8. 在Qt中配置海康工业相机SDK及遇到的问题(报错)

    1.在项目的.pro文件里导入海康工业相机的SDK路径 INCLUDEPATH += \ D:\HKVersion\MVS_3.1.0\MVS\Development\Includes #这时到入Op ...

  9. 哔哩哔哩直播录制工具v1.1.18

    软件介绍 看直播有时候非常精彩想要录制下来,或者非常喜欢某个主播想录制下直播全程,可去找录制软件的时候却发现有这样那样的问题,导致一番操作不尽人意.但是现在<B站直播录制工具>可以完美解决 ...

  10. SpringBoot + SpringSecurity + Quartz + Layui实现系统权限控制和定时任务

    1. 简介   Spring Security是一个功能强大且易于扩展的安全框架,主要用于为Java程序提供用户认证(Authentication)和用户授权(Authorization)功能.    ...