这篇文章深入讲解了 Java 中的 LinkedList,包括基本概念(如链表定义、结点组成、类型等)、单向链表及相关手写实现(如增删查等方法)、特殊的链表(带哨兵的单向链表、双向链表、循环链表)、LinkedList 的源码详解(类的继承关系、成员变量、核心方法等)、关键特性(如双向链表、动态大小、访问特点、增删效率、内存占用等)、优缺点及适用场景。
关联问题:单向链表怎么高效增删双向链表如何遍历LinkedList内存如何优化
前言
LinkedList
是Java
中常用的集合类之一,其核心价值
有如下3
点:
高效的增删操作
。充分利用内存空间
。用作其他数据结构
(如栈
、队列
、双端队列
)。
操千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意。
一、基本概念
1.1、定义
链表是一种物理存储单元上非连续
、非顺序
的存储结构,数据元素的逻辑顺序
是通过链表中的指针链接次序实现的。
1.2、结点
链表由一系列结点
(链表中每一个元素称为结点
)组成,结点可以在运行时动态生成
。
每个结点包括两个
部分:
- 数据域:
存储数据元素
。 - 指针域:
存储下一个结点的地址
。
1.3、链表的类型
1.4、LinkedList的概述
LinkedList
是 java.util
包中的一个类,它实现了 List
接口。基于双向链表
实现,具备高效的增删操作
的能力和充分利用内存空间
,允许在运行时动态生成结点
。
二、单向链表及其手写实现
2.1、特点:
- 每个元素
只知道其下一个元素
,意味着访问链表中的元素必须从头指针
开始。 - 尾结点(
tail
)指向null
,是结束遍历的标识
。
2.2、实现步骤:
- 1、定义一个单向链表类
SingleLinkedList
。 - 2、定义一个结点类
Node
。 - 3、链表是由一系列结点组成,它们之间是
组合关系
,这种组合关系可使用内部类
方式实现。 - 4、根据链表的特点还需要定义一个
头指针
。 - 5、分别实现链表的
遍历
、增
、删
、查
等方法。
完整代码实现
及详细注释
如下:
Java
代码解读
复制代码
import java.util.Iterator; import java.util.function.Consumer; /** * 单向链表 */ public class SingleLinkedList implements Iterable<Integer> { /** * 头指针: 代表链表的起始位置 * 根据头指针才能访问到链表中的其他元素 * 初始时, 头指针指向null, 意味着链表中无结点 */ private Node head = null; public void addFirst(int value) { /** * 1.链表为空: * 创建新结点, 赋值给头指针 * 意味着头指针指向该结点, 然后将结点链入链表中, 此时新节点就是第一个结点 */ // head = new Node(value, null); //2.链表非空: 创建新结点, 其next指向head 然后再让head指向新结点后链入链表 // /** * 2.链表非空: * 创建新结点, 其next指向head * 然后再让head指向新结点后链入链表中 * 代码包含1场景, 保留如下代码即可, 但分析步骤不可少, 可细细体会下该过程 */ head = new Node(value, head); } /** * 查找最后一个结点 */ private Node findLast() { //空链表处理 if (head == null) { return null; } Node p = head; while (p.next != null) { p = p.next; } return p; } /** * 链表尾部添加数据, 需要先知晓最后一个结点 */ public void addLast(int value) { //1.查找到最后一个结点 Node last = findLast(); //2.处理链表为空的场景 if (last == null) { addFirst(value); return; } //3.创建新结点, 新结点指针指向null, 最后一个结点指向新结点 last.next = new Node(value, null); } /** * 根据索引插入数据 * 1.需要先找到该索引的上一个结点 视为前结点 * 2.创建新结点: * a.新结点的next指向前结点的next * b.前结点的next指向新结点 */ public void add(int index, int value) { if (index == 0) { addFirst(value); return; } Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } prev.next = new Node(value, prev.next); } /** * 1.根据索引获取结点的值 索引是在遍历的过程才知晓的 * 2.若在Node中加index属性, 增删时维护起来比较困难 */ private Node findNode(int index) { int i = 0; for (Node p = head; p != null; p = p.next, i++) { if (i == index) { return p; } } return null; } public int get(int index) { Node node = findNode(index); if (node == null) { throw new IndexOutOfBoundsException(); } return node.value; } /** * 删除第一个节点, 通过改变头指针的指向就能实现 */ public void removeFirst() { if (head == null) { throw new IndexOutOfBoundsException(); } head = head.next; } /** * 根据索引删除元素, 关键也是找到上一个结点 */ public void remove(int index) { if (index == 0) { removeFirst(); return; } //查找索引对应的上一个结点 Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } //获取删除的节点 Node removed = prev.next; if (removed == null) { throw new IndexOutOfBoundsException(); } //处理结点直接的指向 prev.next = removed.next; } /** * while 循环遍历 */ public void loop(Consumer<Integer> consumer) { Node p = head; while (p != null) { // 循环中对链表的操作, 最好不要写到循环中, 而是把它当做参数传递进来 // System.out.println(p.value); consumer.accept(p.value); //更新数据 p = p.next; } } /** * for 循环遍历 */ public void loop1(Consumer<Integer> consumer) { for (Node p = head; p != null; p = p.next) { consumer.accept(p.value); } } /** * 迭代器 遍历 */ @Override public Iterator<Integer> iterator() { //此处也可使用匿名内部类 return new NodeIterator(); } /** * 1.结点类: 对结点描述 * 2.静态内部类: 未使用到外部类的属性时使用 * 3.内部类: 使用到外部类的属性时使用 * 4.为啥使用内部类? * 1.与外部类是组合关系(链表是有多个结点组成), 实际应用中, * 若遇到类似场景时, 需要考虑是否使用外部类和内部类组合实现 * 2.封装思想: 避免外部知晓更多信息, * 对外暴露的信息越少越好, 外部不需要知道内部更底层的实现 */ private static class Node { /** * 值 */ int value; /** * 指向下一个结点的指针 */ Node next; public Node(int value, Node next) { this.value = value; this.next = next; } } /** * 内部类 使用到了外部类的head属性, 不能使用static */ private class NodeIterator implements Iterator<Integer> { //指针初始值 Node p = head; @Override public boolean hasNext() { //询问是否有下一个元素 return p != null; } @Override public Integer next() { // 1.返回当前值 int value = p.value; // 2.并指向下一个元素 p = p.next; return value; } } }
三、单向链表(带哨兵)及其手写实现
单向链表中还有一种特殊的结点称为哨兵结点,它不存储数据,通常用作头尾,用来简化边界的判断,可对比一下不带哨兵
的实现。
完整代码实现
及详细注释
如下:
Java
代码解读
复制代码
/** * 单向链表(带哨兵): 主要简化单链表的边界判断 */ public class SentinelLinkedList implements Iterable<Integer> { private Node head = new Node(666, null); public void addFirst(int value) { add(0, value); } public void addLast(int value) { //带哨兵的最后一个结点不可能为null Node last = findLast(); last.next = new Node(value, null); } public void add(int index, int value) { Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } prev.next = new Node(value, prev.next); } public void removeFirst() { remove(0); } public void remove(int index) { //查找索引对应的上一个结点 Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } //获取删除的节点 Node removed = prev.next; if (removed == null) { throw new IndexOutOfBoundsException(); } //处理结点直接的指向 prev.next = removed.next; } private Node findNode(int index) { int i = -1; for (Node p = head; p != null; p = p.next, i++) { if (i == index) { return p; } } return null; } public int get(int index) { Node node = findNode(index); if (node == null) { throw new IndexOutOfBoundsException(); } return node.value; } private Node findLast() { Node p = head; while (p.next != null) { p = p.next; } return p; } @Override public Iterator<Integer> iterator() { //此处也可使用匿名内部类 return new NodeIterator(); } private static class Node { int value; Node next; public Node(int value, Node next) { this.value = value; this.next = next; } } private class NodeIterator implements Iterator<Integer> { //指针初始值 从哨兵的下一个值开始遍历 Node p = head.next; @Override public boolean hasNext() { return p != null; } @Override public Integer next() { int value = p.value; p = p.next; return value; } } }
四、双向链表(带哨兵)及其手写实现
4.1、特点:
- 每个元素
知道其上一个元素和下一个元素
。 - 头结点(
head
)指向null
。 - 尾结点(
tail
)指向null
。
Java
代码解读
复制代码
/** * 双向链表(带哨兵): 主要简化单链表的边界判断 */ public class DoubleLinkedList implements Iterable<Integer> { private final Node head;//头哨兵 private final Node tail;//尾哨兵 public DoubleLinkedList() { head = new Node(null, 666, null); tail = new Node(null, 888, null); head.next = tail; tail.prev = head; } /** * 根据索引位置查找其对应的节点 */ private Node findNode(int index) { int i = -1; for (Node p = head; p != tail; p = p.next, i++) { if (i == index) { return p; } } return null; } public void addFirst(int value) { add(0, value); } public void add(int index, int value) { //查找上一个结点 Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } //查找下一个节点 Node next = prev.next; Node inserted = new Node(prev, value, next); prev.next = inserted; next.prev = inserted; } public void addLast(int value) { //带哨兵的最后一个结点不可能为null Node last = tail.prev; Node added = new Node(last, value, tail); last.prev = added; tail.next = added; } public void removeFirst() { remove(0); } public void remove(int index) { //查找索引对应的上一个结点 Node prev = findNode(index - 1); if (prev == null) { throw new IndexOutOfBoundsException(); } //获取待删除的节点 Node removed = prev.next; if (removed == tail) { throw new IndexOutOfBoundsException(); } //获取待删除的节点的下一个结点 Node next = removed.next; prev.next = next; next.prev = prev; } public void removeLast() { Node removed = tail.prev; if (removed == head) { throw new IndexOutOfBoundsException(); } Node prev = removed.prev; prev.next = tail; tail.prev = prev; } @Override public Iterator<Integer> iterator() { //此处也可使用匿名内部类 return new NodeIterator(); } private static class Node { Node prev; int value; Node next; public Node(Node prev, int value, Node next) { this.prev = prev; this.value = value; this.next = next; } } private class NodeIterator implements Iterator<Integer> { //指针初始值 从哨兵的下一个值开始遍历 Node p = head.next; @Override public boolean hasNext() { return p != tail; } @Override public Integer next() { int value = p.value; p = p.next; return value; } } }
五、循环链表
5.1、特点:
- 每个元素
只知道其下一个元素
。 - 尾结点(
tail
)指向头结点(head
)。 - 实际开发中
不常用
,做简单了解就好。
六、LinkedList的源码详解(基于JDK1.8
)
通过上述手动实现,我们对LinkedList
有了一个初步的认知,接下来深入探索JDK
中LinkedList
源码的实现,继续加深
理解程度。关于如何阅读源码,可从如下3
个方面入手:
- 类的继承关系及接口实现。
- 成员变量和构造函数。
- 核心方法。
图像表示:
6.1、类的继承关系及接口实现
Java
代码解读
复制代码
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable { ... }
知识以图片
的形式进入大脑,记忆更为深刻
。
其他接口在上一章节已详细描述,本章节主要介绍Deque
Deque
是双端队列(Double-Ended Queue
)的缩写,它允许在队列的两端
进行插入和删除
操作。LinkedList
实现了 Deque
接口,提供了双端队列
的功能。
以下是LinkedList
作为Deque
的主要作用:
-
1、两端插入和删除
addFirst(E e)
:在队列头部添加一个元素。addLast(E e)
:在队列尾部添加一个元素。removeFirst()
:删除并返回队列头部的元素。removeLast()
:删除并返回队列尾部的元素。
-
2、两端访问
getFirst()
:返回队列头部的元素,但不删除。getLast()
:返回队列尾部的元素,但不删除。
-
3、迭代器
iterator()
:返回一个从队列头部开始的迭代器。descendingIterator()
:返回一个从队列尾部开始的迭代器。
6.2、成员变量和构造函数
Java
代码解读
复制代码
/** 链表中元素的个数 */ transient int size = 0; /** 头结点 */ transient Node<E> first; //** 尾结点 */ transient Node<E> last; //** 无参数构造 */ public LinkedList() { } /** 集合构造 */ public LinkedList(Collection<? extends E> c) { this(); addAll(c); }
6.3、核心方法(add(index,value)
和node(index)
)
Java
代码解读
复制代码
/** * 添加指定元素到列表的末尾 */ public boolean add(E e) { linkLast(e); return true; } /** * 链入末尾的元素 做简单的指针变更操作 * 1.创建新结点 前驱指向链表中的尾结点 后继指向null * 2.变更尾结点的指向 * 3.若原始尾结点为null,将新结点即为头结点 * 4.若原始尾节点不为null,将尾结点的后继指向新结点 */ void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if (l == null) first = newNode; else l.next = newNode; size++; modCount++; } /** * 向指定的位置插入数据 * 1.下标越界检测 * 2.判断位置是否最后 * 3.查找node的位置 * 4.创建新结点插入指定位置 */ public void add(int index, E element) { checkPositionIndex(index); if (index == size) linkLast(element); else linkBefore(element, node(index)); } /** * 通过折半查找法 * 1. 小于size一半的从前往后遍历获取指定的节点 * 2. 大于size一半的从后往前遍历获取指定的节点 */ 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; } } /** * 对插入的元素做指针变换操作 */ 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++; }
熟练掌握上述的核心方法,删除
、修改
、查询
等操作也类似。就不一一讲述了。LinkedList
相关面试攻略可查看ArrayList
章节。
七、LinkedList
的关键特性
7.1、双向链表:
- 基于
双向链表
实现,每个结点有前驱
、数据域
、后继
,能高效地从两端进行操作
(核心价值)。
7.2、动态大小:
- 其
大小是动态的
,可根据需要增长或缩小。
7.3、较慢的随机访问:
- 随机访问:时间复制度为
O(n)
,因为需要从头或尾开始逐个遍历
结点。 - 顺序访问:虽然
随机访问较慢
,但顺序访问(如使用迭代器
)仍然非常高效。
7.4、高效的增删操作:
- 中间增删:在查找到增删位置的前提下,增删操作复杂度为
O(1)
。 - 两端操作: 其实现了
Deque
接口,能进行高效的两端操作
。
7.5、内存占用:
- 内存开销大:其内部
结点
需要额外的前驱及后继
指针。
7.6、泛型支持:
支持泛型
,可以指定存储的元素类型
。
7.7、非线程安全:
非线程安全的
,如需在多线程
环境中使用,可以使用Collections.synchronizedList
方法将其包装成线程安全的列表。
八、总结
8.1、优点:
高效的增删操作
。充分利用内存空间
。运行时动态生成结点
。
8.2、缺点:
较慢的随机访问
。内存开销较大
。非线程安全
,多线程环境下需要外部同步。
8.3、适用场景:
频繁的任意位置的增删操作
。用作其他数据结构
(如栈
、队列
和双端队列
)。