1. 基本用法

  LinkedList实现了List、Deque、Queue接口,可以按照队列、栈和双端队列的方式进行操作。LinkedList有两个构造方法,一个是默认构造,另一个接受Collection:

public LinkedList()
public LinkedList(Collection<? extends E> c)

  可以按照List操作:

List<Integer> list = new LinkedList<>();
List<Integer> list1 = new LinkedList<>(Arrays.asList(2, 3, 4, 5));

  LinkedList还实现了队列接口Queue,队列的特点是先进先出,在尾部添加数据,在头部删除数据,其接口定义为:

public interface Queue<E> extends Collection<E> {
// 在尾部添加元素
boolean add(E e);
// 在尾部添加元素
boolean offer(E e);
// 返回头部元素,并且从队列中删除
E remove();
// 返回头部元素,并且从队列中删除
E poll();
// 返回头部元素,但不改变队列
E element();
// 返回头部元素,但不改变队列
E peek();
}

  Queue接口扩展了Collection,主要有三种操作,在尾部添加数据(add、offer)、查看头部元素(element、peek)和删除头部元素(remove、poll)。每种操作都有两种形式,区别在于特殊情况的处理不同。特殊情况是指当队列为空或者为满时,为空就是没有元素数据,为满是指队列有长度大小限制,而且已经占满了。LinkedList的实现中,队列长度没有限制,但是其他的Queue的实现可能有。在队列为空时,remove和element会抛出异常NoSuchElementException,而poll和peek返回null;在队列为满时,add会抛出IllegalStateException,而offer只是返回false。

  把LinkedList当做Queue使用:

        Queue<String> queue = new LinkedList<>();
queue.offer("a");
queue.offer("b");
queue.offer("c");
while (queue.peek() != null) {
System.out.println(queue.poll());
}

  栈是一种和队列特点相反的数据结构,它的特点是先进后出,后进先出。Java中没有单独的栈接口,栈的相关方法包括在了表示双端队列的接口Deque中,主要有三个方法:

    // 入栈
void push(E e);
// 出栈
E pop();
// 查看
E peek();

  push表示入栈,在头部添加元素,栈的空间可能是有限的,如果栈满了,push会抛出IllegalStateException;pop表示出栈,返回头部元素,并且从栈中删除,如果栈为空会抛出NoSuchElementException;peek查看栈头部元素,不修改栈,如果栈为空,返回特殊值null。使用方法如下:

        Deque<Integer> stack = new LinkedList<>();
stack.push(1);
stack.push(2);
stack.push(3);
while (stack.peek() != null) {
System.out.println(stack.pop());
}
/**
* output:
* 3
* 2
* 1
*/

  Java中还有一个Stack类,就是栈的意思,它也实现了栈的一些方法,如push、pop、peek等,但它没有实现Deque接口,他是Vector的子类它增加的这些方法也通过synchronized实现了线程安全。由于Vector和Stack内部使用了大量的syncronized做同步操作,效率比较低,已经过时了,具体就不学习了。

  栈和队列都是在两端进行操作,栈只操作头部,队列两端都操作,但只在尾部添加、头部只查看和删除元素。有一个更为通用的操作两端的接口Deque。接口定义如下:

public interface Deque<E> extends Queue<E> {
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E getFirst();
E getLast();
E peekFirst();
E peekLast();
//删除第一次出现的指定元素(从头到尾遍历)
boolean removeFirstOccurrence(Object o);
//删除最后次出现的指定元素(从头到尾遍历)
boolean removeLastOccurrence(Object o);
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
void push(E e);
E pop();
boolean remove(Object o);
// 队列是否包含指定元素
boolean contains(Object o);
public int size();
Iterator<E> iterator();
// 从后往前遍历的迭代器
Iterator<E> descendingIterator();
}

  根据方法名很容易知道作用,稍微不太清晰的做了注释,descendingIterator()方法示例如下:

        Deque<String> deque = new LinkedList<>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> it = deque.descendingIterator();
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
/**
* output:
* d c b a
*/

  下面看下实现原理。

2. 原理

  先来看下LinkedList的内部组成,再分析一些主要方法的实现,代码基于JDK8

2.1内部组成

  我们知道,ArrayList内部是数组,元素在内存中是连续存放的,基于索引的访问效率非常高,但LinkedList不是。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;
}
}

  Node类表示节点,item指向实际的元素,next后一个节点,prev指向前一个节点。LinkedList内部组成就是如下三个实例变量:

    transient int size = 0;
transient Node<E> first;
transient Node<E> last;

  size表示链表长度,默认为0,first指向头节点,last指向尾节点,初始值都是null。LinkedList的所有public方法内部操作的就是这三个实例变量,来看下具体方法:

2.2 add方法

    public boolean add(E e) {
linkLast(e);
return true;
} void linkLast(E e) {
// 将尾节点赋给l变量
final Node<E> l = last;
// 新建节点,将l赋给新建节点的pre前驱节点,e为当前节点的元素值,新建节点没有后继节点,所以为null
final Node<E> newNode = new Node<>(l, e, null);
// 将新建节点赋给尾节点
last = newNode;
// 如果尾节点不存在,就将新建节点作为头结点赋给first实例变量
if (l == null)
first = newNode;
// 如若尾节点存在,就将新建节点作为尾节点的后继节点赋给l.next
else
l.next = newNode;
// 链表长度加1
size++;
// 修改次数加1
modCount++;
}

  代码的基本步骤见代码中注释,modCount变量用来记录修改次数,便于在迭代中检测结构性变化。我们根据图示来理解下。比如如下代码:

    List<String> list = new LinkedList<String>();
list.add("a");
list.add("b");

  

  当新建list对象后内部结构如图一,头结点和尾节点都是null;当添加“a”后内部结构如图二,size加1,头结点和尾节点都指向同一个Node节点;当添加完“b”后内部结构如图三所示。

2.3 根据索引访问元素

  来看下get方法:

    public E get(int index) {
// 检查索引位置的有效性,若无效,抛出异常
checkElementIndex(index);
// 索引有效,执行node方法,查找指定索引位置的元素并返回
return node(index).item;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
// 抛出未受检异常,索引越界异常
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
Node<E> node(int index) {
// 若索引位置在前半部分,则从头结点开始查找(右移一位相当于除以2),若找到返回节点
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;
}
}

  与ArrayList不同,ArrayList中数组元素连续存放,可以根据索引直接定位,而在LinkedList中,则必须从头到尾顺着连接查找,效率比较低。

2.4 按内容查找元素

  看下indexOf的代码:

    public int indexOf(Object o) {
int index = 0;
// 查找元素为null时
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null)
return index;
index++;
}
// 查找元素不为null时
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item))
return index;
index++;
}
}
// 买找到指定元素返回-1
return -1;
}

  代码比较简单,有两种情况,都是从头节点开始找,见代码注释。

2.5 插入元素

  add是在尾部添加元素,如果在头部或者中间插入元素可以使用如下重载方法:

    public void add(int index, E element) {
checkPositionIndex(index);
// 这就是在尾部添加元素
if (index == size)
linkLast(element);
// 主要看这个
else
linkBefore(element, node(index));
}
void linkBefore(E e, Node<E> succ) {
// succ不为空,就把succ的前驱节点赋给pred
final Node<E> pred = succ.prev;
// 新建节点,将pred指定为新建节点的前驱节点,succ为后继节点
final Node<E> newNode = new Node<>(pred, e, succ);
// 将后的前驱指向新建节点
succ.prev = newNode;
// 将前驱的后继指向新建节点,若前驱为空,修改头结点指向新节点
if (pred == null)
first = newNode;
else
pred.next = newNode;
// 增加长度
size++;
modCount++;
}

  下面通过图示来加深理解,比如添加一个元素

    list.add(1, "c");

  

  可以看出,在中间插入元素,LinkedList只需按需分配内存,修改前驱和后继节点的链接,而ArrayList则可能需要分配很多的额外空间,且移动、复制所有后继元素。

2.6 删除元素

  再来看看删除元素的代码:

    public E remove(int index) {
// 同上检查索引是否有效
checkElementIndex(index);
// node方法先查找节点,再执行unlink删除指定节点
return unlink(node(index));
}
E unlink(Node<E> x) {
// x节点不为空,取得节点元素值
final E element = x.item;
// 取得前驱节点
final Node<E> next = x.next;
// 取得后继节点
final Node<E> prev = x.prev;
// 指定前驱的后继为x的后继(不在指向x),若前驱为空,修改头节点指向x的后继
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
// 指定后继的前驱为x的前驱(不再指向x),若后继为空,修改尾节点指向x的前驱
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
// x节点元素值设为null,便于垃圾回收
x.item = null;
// 链表长度减1
size--;
// 修改次数加1
modCount++;
// 返回删除的节点值
return element;
}

  分析逻辑见代码注释,基本思路就是让x的前驱和后继直接链接起来,再把x的前驱、后继节点、item都设置为null,便于垃圾回收。下面通过图示加深理解,比如删除一个元素:

list.remove(1);

3. LinkedList特点总结

  用法上LinkedList是一个List,有序有重复元素,也实现了Deque接口,可以作为队列、栈和双端队列使用。实现原理上,LinkedList内部是一个双向链表,并维护了长度、头结点和尾节点。有如下特点:

  1. 按需分配空间,不需要预先分配很多空间。

  2. 不可以随机访问,按照索引位置访问效率比较低,必须从头或尾顺着链接找,效率为O(N/2)。

  3. 不管列表是否已排序,只要按照内容查找元素,效率都比较低,必须逐个比较,效率为O(N)。

---------- I love three things in this world. Sun, moon and you. Sun for morning, moon for night , and you forever .

Java LinkedList小记的更多相关文章

  1. effective java读书小记(一)创建和销毁对象

    序言 <effective java>可谓是java学习者心中的一本绝对不能不拜读的好书,她对于目标读者(有一点编程基础和开发经验)的人来说,由浅入深,言简意赅.每一章节都分为若干的条目, ...

  2. java LinkedList(链表)

    LinkedList也像ArrayList一样实现了基本的List接口,但是它执行某些操作(在List的中间插入和移除)时比ArrayList更高效,但在随机访问方面却要逊色一些 LinkedList ...

  3. Java LinkedList add vs push

    Java LinkedList add 是加在list尾部. LinkedList push 施加在list头部. 等同于addFirst.

  4. Java LinkedList【笔记】

    Java LinkedList[笔记] LinkedList LinkedList 适用于要求有顺序,并且会按照顺序进行迭代的场景,依赖于底层的链表结构 LinkedList基本结构 LinkedLi ...

  5. java LinkedList (详解)

    Java 链表(LinkedList) 一.链表简介 1.链表 (Linked List) 是一种常见的基础数据结构,是一种线性表,但是链表不会按线性表的顺序存储数据,而是每个节点里存到下一个节点的地 ...

  6. Java LinkedList 源码剖析

    LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack).这样看来,LinkedList简直就 ...

  7. java@ LinkedList 学习

    package abc.com; import java.util.LinkedList; public class TestLinkedList { static void prt(Object o ...

  8. JAVA LinkedList和ArrayList的使用及性能分析

    第1部分 List概括List的框架图List 是一个接口,它继承于Collection的接口.它代表着有序的队列.AbstractList 是一个抽象类,它继承于AbstractCollection ...

  9. [Java] LinkedList / Queue - 源代码学习笔记

    简单地画了下 LinkedList 的继承关系,如下图.只是画了关注的部分,并不是完整的关系图.本博文涉及的是 Queue, Deque, LinkedList 的源代码阅读笔记.关于 List 接口 ...

随机推荐

  1. linux与windows下文件编码问题

    注:转换操作均在Linux终端进行操作 DOS与Unix格式转换 安装工具:dos2unix.unix2dos # ubuntu apt-get install dos2unix apt-get in ...

  2. python3中的bytes和string

    原文链接:https://www.cnblogs.com/abclife/p/7445222.html python 3中最重要的新特性可能就是将文本(text)和二进制数据做了更清晰的区分.文本总是 ...

  3. 攻防世界 杂项 4.something_in_image

    这是原题 我这里使用编辑器打开,一看乱码也挺多的,于是想了想ctrl+f搜索一下flag关键字吧,结果答案出来了(flag不少,多搜索几次) Flag{yc4pl0fvjs2k1t7T}

  4. 单源最短路径算法:迪杰斯特拉 (Dijkstra) 算法(二)

    一.基于邻接表的Dijkstra算法 如前一篇文章所述,在 Dijkstra 的算法中,维护了两组,一组包含已经包含在最短路径树中的顶点列表,另一组包含尚未包含的顶点.使用邻接表表示,可以使用 BFS ...

  5. arduino 使用 analogRead 读取不到数据,digitalRead 却可以正常读取

    项目场景: 最近在使用安信可的 ESP32S P14 引脚(ADC 16)读取一个电路状态的时候遇到一个问题,电路状态不是很稳定,在高电平的时候,会突然出现毫秒级的波动,出现短暂的低电平,造成设备状态 ...

  6. python网站(持续更新)

    python官网: https://www.python.org/ python文档:中文 https://docs.python.org/zh-cn/3/ pypi网站: https://pypi. ...

  7. P2598 [ZJOI2009]狼和羊的故事(最小割)

    P2598 [ZJOI2009]狼和羊的故事 说真的,要多练练网络流的题了,这么简单的网络流就看不出来... 题目要求我们要求将狼和羊分开,也就是最小割,(等等什么逻辑...头大....) 我们这样想 ...

  8. hdu 3863 No Gambling (不会证明,但是是对的,,)

    题意: N=4时 规则: 双方每次可以连接自己颜色的两个点(相邻,长度为1),线和线不能交叉重叠. 蓝方要连接左右,红方要连接上下. 蓝方先.问谁先连接? 思路: 经过观察....蓝方胜....... ...

  9. 浅谈对typora的使用

    内容概要 - 什么是typora - typora的具体使用 目录 内容概要 - 什么是typora - typora的具体使用 1. 什么是typora 2.typora的具体使用 1.标题级别 2 ...

  10. 【数据结构&算法】04-线性表

    目录 前言 线性表的定义 线性表的数据类型&操作 线性表操作 数据类型定义 复杂操作 线性表的顺序存储结构 顺序存储结构的定义 顺序存储方式 数据长度和线性表长度的区别 地址的计算方法 顺序存 ...