一、概述

先来看看源码中的这一段注释,我们先尝试从中提取一些信息:

Doubly-linked list implementation of the List and Deque interfaces. Implements all optional list operations, and permits all elements (including null).

All of the operations perform as could be expected for a doubly-linked list. Operations that index into the list will traverse the list from the beginning or the end, whichever is closer to the specified index.

Note that this implementation is not synchronized. If multiple threads access a linked list concurrently, and at least one of the threads modifies the list structurally, it must be synchronized externally. (A structural modification is any operation that adds or deletes one or more elements; merely setting the value of an element is not a structural modification.) This is typically accomplished by synchronizing on some object that naturally encapsulates the list.

从这段注释中,我们可以得知 LinkedList 是通过一个双向链表来实现的,它允许插入所有元素,包括null,同时,它是线程不同步的。

如果对双向链表这个数据结构很熟悉的话,学习 LinkedList 就没什么难度了。下面是双向链表的结构:

如果对双向链表这个数据结构很熟悉的话,学习 LinkedList 就没什么难度了。下面是双向链表的结构:

双向链表每个结点除了数据域之外,还有一个前指针和后指针,分别指向前驱结点和后继结点(如果有前驱/后继的话)。另外,双向链表还有一个first指针,指向头节点,和last指针,指向尾节点。

二、属性

接下来看一下 LinkedList 中的属性:

//链表的节点个数
transient int size = 0; //指向头节点的指针
transient Node<E> first; //指向尾节点的指针
transient Node<E> last;

LinkedList 的属性非常少,就只有这些。通过这三个属性,其实我们大概也可以猜测出它是怎么实现的了。

三、方法

1、结点结构

Node 是在 LinkedList 里定义的一个静态内部类,它表示链表每个节点的结构,包括一个数据域item,一个后置指针next,一个前置指针prev。

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;
}
}
2、添加元素

对于链表这种数据结构来说,添加元素的操作无非就是在表头/表尾插入元素,又或者在指定位置插入元素。因为 LinkedList 有头指针和尾指针,所以在表头或表尾进行插入元素只需要 O(1) 的时间,而在指定位置插入元素则需要先遍历一下链表,所以复杂度为 O(n)。

在表头添加元素的过程如下:



当向表头插入一个节点时,很显然当前节点的前驱一定为 null,而后继结点是 first 指针指向的节点,当然还要修改 first 指针指向新的头节点。除此之外,原来的头节点变成了第二个节点,所以还要修改原来头节点的前驱指针,使它指向表头节点,源码的实现如下:

private void linkFirst(E e) {
final Node<E> f = first;
//当前节点的前驱指向null,后继指针原来的头节点
final Node<E> newNode = new Node<>(null, e, f);
//头指针指向新的头节点
first = newNode;
//如果原来有头节点,则更新原来节点的前驱指针,否则更新尾指针
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}

在表尾添加元素跟在表头添加元素大同小异,如图所示:

当向表尾插入一个节点时,很显然当前节点的后继一定为 null,而前驱结点是 last 指针指向的节点,然后还要修改 last 指针指向新的尾节点。此外,还要修改原来尾节点的后继指针,使它指向新的尾节点,源码的实现如下:

void linkLast(E e) {
final Node<E> l = last;
//当前节点的前驱指向尾节点,后继指向null
final Node<E> newNode = new Node<>(l, e, null);
//尾指针指向新的尾节点
last = newNode;
//如果原来有尾节点,则更新原来节点的后继指针,否则更新头指针
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

最后,在指定节点之前插入,如图所示:

当向指定节点之前插入一个节点时,当前节点的后继为指定节点,而前驱结点为指定节点的前驱节点。此外,还要修改前驱节点的后继为当前节点,以及后继节点的前驱为当前节点,源码的实现如下:

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

3、删除元素

删除操作与添加操作大同小异,例如删除指定节点的过程如下图所示,需要把当前节点的前驱节点的后继修改为当前节点的后继,以及当前节点的后继结点的前驱修改为当前节点的前驱(是不是很绕?):

删除头节点和尾节点跟删除指定节点非常类似,就不一一介绍了,源码如下:

//删除表头节点,返回表头元素的值
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;
f.next = null; // help GC
first = next; //头指针指向后一个节点
if (next == null)
last = null;
else
next.prev = null; //新头节点的前驱为null
size--;
modCount++;
return element;
}
//删除表尾节点,返回表尾元素的值
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev; //尾指针指向前一个节点
if (prev == null)
first = null;
else
prev.next = null; //新尾节点的后继为null
size--;
modCount++;
return element;
}
//删除指定节点,返回指定元素的值
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next; //当前节点的后继
final Node<E> prev = x.prev; //当前节点的前驱 if (prev == null) {
first = next;
} else {
prev.next = next; //更新前驱节点的后继为当前节点的后继
x.prev = null;
} if (next == null) {
last = prev;
} else {
next.prev = prev; //更新后继节点的前驱为当前节点的前驱
x.next = null;
} x.item = null;
size--;
modCount++;
return element;

}

4、获取元素

获取元素的方法一看就懂,我就不必多加解释了。

//获取表头元素
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
} //获取表尾元素
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
} //获取指定下标的元素
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;
}
}
5、常用方法

前面介绍了链表的添加和删除操作,你会发现那些方法都不是 public 的,LinkedList 是在这些基础的方法进行操作的,下面就来看看我们可以调用的方法有哪些。

//删除表头元素
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
//删除表尾元素
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
//插入新的表头节点
public void addFirst(E e) {
linkFirst(e);
}
//插入新的表尾节点
public void addLast(E e) {
linkLast(e);
}
//链表的大小
public int size() {
return size;
}
//添加元素到表尾
public boolean add(E e) {
linkLast(e);
return true;
}
//删除指定元素
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;
}
//获取指定下标的元素
public E get(int index) {
checkElementIndex(index); //先检查是否越界
return node(index).item;
}
//替换指定下标的值
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
//在指定位置插入节点
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
//删除指定下标的节点
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
//获取表头节点的值,表头为空返回null
public E peek() {
final Node<E> f = first;
return (f == null) ? null : f.item;
}
//获取表头节点的值,表头为空抛出异常
public E element() {
return getFirst();
}
//获取表头节点的值,并删除表头节点,表头为空返回null
public E poll() {
final Node<E> f = first;
return (f == null) ? null : unlinkFirst(f);
}
//添加元素到表头
public void push(E e) {
addFirst(e);
}
//删除表头元素
public E pop() {
return removeFirst();
}

四、总结

  1. LinkedList 的底层结构是一个带头/尾指针的双向链表,可以快速的对头/尾节点进行操作。
  2. 相比数组,链表的特点就是在指定位置插入和删除元素的效率较高,但是查找的效率就不如数组那么高了。

集合总结二(LinkedList的实现原理)的更多相关文章

  1. 【源码阅读】Java集合之二 - LinkedList源码深度解读

    Java 源码阅读的第一步是Collection框架源码,这也是面试基础中的基础: 针对Collection的源码阅读写一个系列的文章; 本文是第二篇LinkedList. ---@pdai JDK版 ...

  2. 2018/1/8 学习汇总,kettle简单介绍,集合遍历方式的选择及原理,防止表单重复提交的后台解决方案

    昨天因为当前这个二次开发项目的接近尾声,要求我们将生产环境数据库里的数据迁移到现在新的数据库来,但老数据库里是sqlserver而新数据库则是ORACLE,不仅仅面对着数据库数据类型结构不一致的问题, ...

  3. List、Set集合系列之剖析HashSet存储原理(HashMap底层)

    目录 List接口 1.1 List接口介绍 1.2 List接口中常用方法 List的子类 2.1 ArrayList集合 2.2 LinkedList集合 Set接口 3.1 Set接口介绍 Se ...

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

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

  5. Java面试集合(二)

    前言 大家好,给大家带来Java面试集合(二)的概述,希望你们喜欢 二 1.请问线程有哪些状态? 新建状态(New) 就绪状态(Runnable) 运行状态(Running) 阻塞状态(Blocked ...

  6. Java LinkedList的实现原理

    LinkedList是Java List类型的集合类的一种实现,此外,LinkedList还实现了Deque接口.本文基于Java1.8,对于LinkedList的实现原理做一下详细讲解. (Java ...

  7. java集合系列之LinkedList源码分析

    java集合系列之LinkedList源码分析 LinkedList数据结构简介 LinkedList底层是通过双端双向链表实现的,其基本数据结构如下,每一个节点类为Node对象,每个Node节点包含 ...

  8. Java集合框架之LinkedList浅析

    Java集合框架之LinkedList浅析 一.LinkedList综述: 1.1LinkedList简介 同ArrayList一样,位于java.util包下的LinkedList是Java集合框架 ...

  9. Java中的集合Queue、LinkedList、PriorityQueue(四)

    Queue接口 Queue用于模拟了队列这种数据结构,队列通常是指“先进先出”(FIFO)的容器.队列的头部保存在队列中时间最长的元素,队列的尾部保存在队列中时间最短的元素.新元素插入(offer)到 ...

随机推荐

  1. Vue实现购物车小球动画

    思路: 1.因页面分组件分的比较细,由图可知是组件5到组件4的联动. 如果利用组件间通信需要 子组件5 -->组件3-->所有组件的父组件-->组件4, 层级略显复杂,所以使用了vu ...

  2. redhat7.2安全基线BI

    (一)   Redhat linux7.2安全基线基本型(BI) 1.   密码复杂度策略 /etc/pam.d/system-auth文件中,增加内容 password requisite pam_ ...

  3. 2017 Russian Code Cup (RCC 17), Final Round

    2017 Russian Code Cup (RCC 17), Final Round A Set Theory 思路:原题转换一下就是找一个b数组,使得b数组任意两个数的差值都和a数组任意两个数的差 ...

  4. Practical Node.js (2018版) 第9章: 使用WebSocket建立实时程序,原生的WebSocket使用介绍,Socket.IO的基本使用介绍。

    Real-Time Apps with WebSocket, Socket.IO, and DerbyJS 实时程序的使用变得越来越广泛,如传统的交易,游戏,社交,开发工具DevOps tools, ...

  5. Windows 2003 Server R2 x64 IIS6.0 eWebEditor无法显示的问题

    64位的操作系统安装成功后,一般IIS也是默认的64位的解析,所以将程序放到64位操作系统上的时候,会出现IIS错误,解决方案也非常简单,将64位IIS停止,启动32位的就可以了. 输入以下命令启用 ...

  6. 基于socket实现http请求

    异步非阻塞模块原理 # 基于socket实现http请求 import socket # 多路IO复用模块 import select socket_list= [] url_list = [&quo ...

  7. K-临近算法(KNN)

    K-临近算法(KNN) K nearest neighbour 1.k-近邻算法原理 简单地说,K-近邻算法采用测量不同特征值之间的距离方法进行分类. 优点:精度高.对异常值不敏感.无数据输入假定. ...

  8. sql server替换字段中的某个字符

    USE [Vocabulary ] GO --UPDATE [dbo].[table name] --   SET [en] = '' --      ,[cn] ='' -- WHERE --cha ...

  9. 码云git使用五(创建远程分支和更新远程分支)

    1.创建一个与远程分支没有关联的本地分支 2.从远程拉取到本地分支 3.创建远程分支() 4.搞定了.

  10. inline元素、block元素、inline-block元素

    inline 内联元素:是不可以控制宽和高.margin等:并且在同一行显示,不换行,直到该行排满. block 块级元素:是可以控制宽和高.margin等,并且会换行.块级对象元素会单独占一行显示, ...