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. bzoj2064分裂(dp)

    题目大意: 给定一个初始集合和目标集合,有两种操作:1.合并集合中的两个元素,新元素为两个元素之和 2.分裂集合中的一个元素,得到的两个新元素之和等于原先的元素.要求用最小步数使初始集合变为目标集合, ...

  2. g++ 常用命令

    g++ --help

  3. 【转-Andrew_qian】stm32中断嵌套全攻略

    断断续续学习STM32一学期了,时间过的好快,现在对STM32F103系列单片机的中断嵌套及外部中断做一个总结,全当学习笔记.废话不多说,ARM公司的Cortex-m3 内核,支持256个中断,其中包 ...

  4. 3.2 Dependencies of the Projects in the Solution 解决方案中项目间的依赖项

    3.2 Dependencies of the Projects in the Solution 解决方案中项目间的依赖项 The diagram below shows the essential ...

  5. kafka应用讲解及应用场景(三)

    一. 验证 1.进入bin目录 cd bin 2.ls查看脚本 会发现下面有很多脚本文件,由于我是要创建一个topic所有直接打开kafka-topics.sh脚本查看命令 打开脚本后发现里面有很多命 ...

  6. Linux上传下载神器之 lrzsz

    在开发的过程中,经常遇到 需要在 Linux 和 Windows 之间上传下载文件的情况 这时,一般都是使用 FTP 或者 WinSCP 工具进行上传下载, 虽然也能解决问题,但是这些工具需要在本地安 ...

  7. leetcode 5/300 最长回文子串 py

    目录 题目说明 方法一:动态规划--状态转移方程 方法二:优化中心扩展算法 题目说明 要看明白求得是什么,最长回文字串是指例如cababa中ababa是最长的,不是求回文的部分aba 方法一:动态规划 ...

  8. Beta阶段第七次会议

    Beta阶段第七次会议 时间:2020.5.23 完成工作 姓名 工作 难度 完成度 ltx 1.修改小程序页面无法加载bug2.修改条件语句,使得页面能够正常显示 中 90% xyq 1.根据api ...

  9. seata整合nacos完成分布式的部署

    seata整合nacos完成分布式的部署 一.背景 二.部署机器 三.部署步骤 1.在seata上创建命名空间 2.下载对应版本的seata 3.单机启动 1.修改seata配置文件 1.修改注册中心 ...

  10. SCons - 简单而强大的项目编译脚本(原文https://www.cnblogs.com/binchen-china/p/5646791.html)

    N年前学的makefile,当时还勉强能写一些简单的工程编译,现在已经基本忘了.makefile确实编写复杂,而且平时也不是经常使用,容易忘记.偶识了scons,一切都变的简单了.最近研究了下scon ...