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. App界面

    首先我直接放图,存储记录一下,自己开发的app,后端是java分布式,

  2. 初中的一些OI琐屑 & APIO2020 & NOI2020

    这篇文章会发布在我的博客上 https://www.cnblogs.com/dmoransky/(一个小习惯,把信息学竞赛的学习历程记录在个人博客中). 借这篇随笔回顾并简短总结一下我的初中OI(信息 ...

  3. 本地安装yum源脚本

    rpm -qa|grep yum   //检查是否安装了yum. 如果没有安装就执行下面的文件 创建一个以xxx.sh结尾的文件 #!/bin/bash #创建两个文件用于挂载文件 mkdir /mn ...

  4. IIS-logfiles详解以及日志日期问题

    IIS日志的含义IIS是Internet Information Server的缩写,意思是英特网信息服务,日志就是运行的记录 IIS日志的默认目录就是%systemroot%\system32\lo ...

  5. Docker部署FastDFS(附示例代码)

    1. FastDFS简介   FastDFS是一个开源的分布式文件系统,它对文件进行管理,功能包括:文件存储.文件同步.文件访问(文件上传.文件下载)等,解决了大容量存储和负载均衡的问题.特别适合以文 ...

  6. php项目从github自动pull到服务器

    php项目github自动pull到服务器 项目名:web 一.自动触发 1.在服务器添加脚本文件:gitpull.sh #!/bin/sh cd /www/web git reset --hard ...

  7. DataGrid 字体垂直居中

    如果用DataGridTextColumn作为DataGrid的列,字体垂直居中需要这样设置: <Style x:Key="Body_Content_DataGrid_Centerin ...

  8. 常见的 emit 实现 AOP demo

    0. 前言 上接:思想无语言边界:以 cglib 介绍 AOP 在 java 的一个实现方式 作为第四篇,我们回顾一下 csharp 里面比较常见动态编织实现方式emit 内容安排如下: emit a ...

  9. disable_functions Bypass

    参考文章和poc(文中均有poc下载地址) : https://www.uedbox.com/post/59295/ https://www.uedbox.com/post/59402/ 当然 fre ...

  10. 使用GitHub发布自己的静态网站

    可参考GitHub官方文档 https://pages.github.com/ https://help.github.com/ 1.在本地新建一个文件夹.然后在文件夹中用git初始化文件夹 git ...