本系列可作为JAVA学习系列的笔记,文中提到的一些练习的代码,小编会将代码复制下来,大家复制下来就可以练习了,方便大家学习。
点赞关注不迷路!您的点赞、关注和收藏是对小编最大的支持和鼓励!
系列文章目录
拓展目录
手把手教你用 ArrayList 实现杨辉三角:从逻辑推导到每行代码详解
目录
目录
[一、ArrayList 的缺陷:链表出现的必然性](#一、ArrayList 的缺陷:链表出现的必然性)
[1.1 ArrayList 底层结构回顾](#1.1 ArrayList 底层结构回顾)
[1.2 ArrayList 的核心缺陷](#1.2 ArrayList 的核心缺陷)
[1.2.1 插入操作的效率瓶颈](#1.2.1 插入操作的效率瓶颈)
[1.2.2 删除操作的效率问题](#1.2.2 删除操作的效率问题)
[1.2.3 扩容带来的额外开销](#1.2.3 扩容带来的额外开销)
[1.3 小结:ArrayList 的适用场景](#1.3 小结:ArrayList 的适用场景)
[2.1 链表的定义与核心特征](#2.1 链表的定义与核心特征)
[2.1.1 链表的概念](#2.1.1 链表的概念)
[2.1.2 链表与数组的核心对比](#2.1.2 链表与数组的核心对比)
[2.1.3 链表的节点结构](#2.1.3 链表的节点结构)
[2.2 链表的分类:8 种组合结构](#2.2 链表的分类:8 种组合结构)
[2.2.1 分类详解](#2.2.1 分类详解)
[2.2.2 8 种组合结构](#2.2.2 8 种组合结构)
[2.3 重点掌握的两种链表结构](#2.3 重点掌握的两种链表结构)
[2.3.1 无头单向非循环链表](#2.3.1 无头单向非循环链表)
[2.3.2 无头双向循环链表](#2.3.2 无头双向循环链表)
[2.4 链表的核心操作思想](#2.4 链表的核心操作思想)
[2.4.1 插入操作(单向链表示例)](#2.4.1 插入操作(单向链表示例))
[2.4.2 删除操作(单向链表示例)](#2.4.2 删除操作(单向链表示例))
[2.4.3 查找操作](#2.4.3 查找操作)
[3.1 节点类定义](#3.1 节点类定义)
[3.2 链表核心操作实现](#3.2 链表核心操作实现)
[3.3 测试代码](#3.3 测试代码)
[3.4 实现要点](#3.4 实现要点)
[四、链表经典面试题解析(10 道核心题)](#四、链表经典面试题解析(10 道核心题))
[4.1 题目 1:删除链表中等于给定值 val 的所有节点(LeetCode 203)](#4.1 题目 1:删除链表中等于给定值 val 的所有节点(LeetCode 203))
[4.2 题目 2:反转一个单链表(LeetCode 206)](#4.2 题目 2:反转一个单链表(LeetCode 206))
[4.3 题目 3:链表的中间结点(LeetCode 876)](#4.3 题目 3:链表的中间结点(LeetCode 876))
[4.4 题目 4:链表中倒数第 k 个结点(剑指 Offer 22)](#4.4 题目 4:链表中倒数第 k 个结点(剑指 Offer 22))
[4.5 题目 5:合并两个有序链表(LeetCode 21)](#4.5 题目 5:合并两个有序链表(LeetCode 21))
[4.6 题目 6:以给定值 x 为基准分割链表(LeetCode 86)](#4.6 题目 6:以给定值 x 为基准分割链表(LeetCode 86))
[4.7 题目 7:链表的回文结构(剑指 Offer 27)](#4.7 题目 7:链表的回文结构(剑指 Offer 27))
[4.8 题目 8:两个链表的第一个公共节点(剑指 Offer 52)](#4.8 题目 8:两个链表的第一个公共节点(剑指 Offer 52))
[4.9 题目 9:判断链表中是否有环(LeetCode 141)](#4.9 题目 9:判断链表中是否有环(LeetCode 141))
[4.10 题目 10:链表入环的第一个节点(LeetCode 142)](#4.10 题目 10:链表入环的第一个节点(LeetCode 142))
[五、无头双向循环链表的实现(LinkedList 底层模拟)](#五、无头双向循环链表的实现(LinkedList 底层模拟))
[5.1 节点类定义](#5.1 节点类定义)
[5.2 链表核心操作实现](#5.2 链表核心操作实现)
[5.3 测试代码](#5.3 测试代码)
[5.4 双向循环链表的优势与实现要点](#5.4 双向循环链表的优势与实现要点)
[六、LinkedList 的使用指南(Java 集合框架)](#六、LinkedList 的使用指南(Java 集合框架))
[6.1 LinkedList 的类结构与核心特性](#6.1 LinkedList 的类结构与核心特性)
[6.2 LinkedList 的构造方法](#6.2 LinkedList 的构造方法)
[6.3 LinkedList 的核心方法(List 接口)](#6.3 LinkedList 的核心方法(List 接口))
[6.4 LinkedList 的双端队列方法(Deque 接口)](#6.4 LinkedList 的双端队列方法(Deque 接口))
[6.5 LinkedList 的遍历方式](#6.5 LinkedList 的遍历方式)
[七、ArrayList 与 LinkedList 的核心区别与应用场景](#七、ArrayList 与 LinkedList 的核心区别与应用场景)
[7.1 核心区别对比](#7.1 核心区别对比)
[7.2 应用场景选择](#7.2 应用场景选择)
[优先使用 ArrayList 的场景](#优先使用 ArrayList 的场景)
[优先使用 LinkedList 的场景](#优先使用 LinkedList 的场景)
[7.3 常见误区澄清](#7.3 常见误区澄清)
[8.1 核心知识点总结](#8.1 核心知识点总结)
[8.2 拓展学习建议](#8.2 拓展学习建议)
前言
小编作为新晋码农一枚,会定期整理一些写的比较好的代码,作为自己的学习笔记,会试着做一下批注和补充,如转载或者参考他人文献会标明出处,非商用,如有侵权会删改!欢迎大家斧正和讨论!
在 Java 集合框架中,List 接口的两大核心实现类 ------ArrayList 和 LinkedList,分别对应数组与链表两种基础数据结构。上一篇我们深入剖析了 ArrayList 的底层原理与使用场景,而本篇将聚焦 LinkedList 及其依赖的链表结构。从 ArrayList 的缺陷出发,逐步拆解链表的概念、结构、实现逻辑、经典面试题、LinkedList 的模拟与实战使用,最后通过对比两者的核心差异,帮助大家构建完整的链表知识体系,为算法学习和开发实践打下坚实基础。
本文涵盖链表相关所有核心知识点,总字数超 30000 字,包含理论讲解、代码实现、面试题深度解析、API 实战指南等内容,适合 Java 初学者、数据结构入门者及需要巩固链表知识的开发人员。建议结合代码示例逐节研读,复杂知识点可反复琢磨,确保真正理解并掌握。
一、ArrayList 的缺陷:链表出现的必然性
要理解链表(LinkedList)的设计初衷,首先需要明确 ArrayList 的核心局限性 ------ 正是这些缺陷,推动了链表这种数据结构的诞生。
1.1 ArrayList 底层结构回顾
通过之前的学习,我们知道 ArrayList 底层基于动态数组实现,其核心源码如下(关键部分截取):
java
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
// 默认初始容量为 10
private static final int DEFAULT_CAPACITY = 10;
// 存储元素的核心数组(transient 表示不参与序列化)
transient Object[] elementData;
// 集合中有效元素的个数
private int size;
// 构造方法:指定初始容量
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
// 其他方法(add、remove、get 等)...
}
从源码可见,ArrayList 的元素存储在一段连续的物理空间 中(即数组 elementData),这种结构带来了随机访问高效的优势,但也存在无法回避的缺陷。
1.2 ArrayList 的核心缺陷
ArrayList 的主要短板集中在任意位置的插入和删除操作,具体分析如下:
1.2.1 插入操作的效率瓶颈
当在 ArrayList 非末尾位置插入元素时,由于数组物理空间连续,插入位置之后的所有元素都需要整体向后搬移一位,才能腾出空间给新元素。
示例:现有容量为 5 的 ArrayList,元素为 [1,2,3,4,5],若在索引 1 处插入 6,需将索引 1~4 的元素(2,3,4,5)依次后移,最终数组变为 [1,6,2,3,4,5](若容量不足还需先扩容)。
这个搬移过程的时间复杂度为 O(n)(n 为元素个数),当元素数量庞大时,频繁插入会导致效率极低。
1.2.2 删除操作的效率问题
与插入类似,删除 ArrayList 非末尾元素时,删除位置之后的所有元素需整体向前搬移一位,填补删除留下的 "空位"。
示例:删除上述数组中索引 2 的元素 2,需将索引 3~5 的元素(3,4,5)依次前移,最终数组变为 [1,6,3,4,5]。
该过程的时间复杂度同样为 O(n),元素越多,删除效率越低。
1.2.3 扩容带来的额外开销
ArrayList 容量固定(初始为 10),当元素个数达到容量上限时,会触发扩容机制 ------ 默认扩容为原容量的 1.5 倍(源码逻辑:newCapacity = oldCapacity + (oldCapacity >> 1))。扩容过程需经历:
- 创建新的更大容量数组;
- 复制原数组所有元素到新数组;
- 释放原数组内存。
这个过程消耗额外时间和空间,元素数量越大,扩容开销越明显。
1.3 小结:ArrayList 的适用场景
ArrayList 优势在于随机访问 (通过索引获取元素,O (1))和尾部插入 / 删除(无需搬移元素,O (1)),适合:
- 元素数量稳定,不频繁进行任意位置插入 / 删除;
- 需频繁通过索引访问元素的场景。
而当场景中存在大量任意位置插入 / 删除时,ArrayList 效率低下,此时需要链表这种数据结构来解决问题。
二、链表:数据结构的核心概念与结构
2.1 链表的定义与核心特征
2.1.1 链表的概念
链表是一种物理存储非连续 、逻辑顺序通过引用链接实现的数据结构。简单来说:
- 物理存储:链表的元素(称为 "节点")分散在内存中,物理地址不一定连续;
- 逻辑顺序:每个节点包含 "引用"(指针),指向其下一个(或上一个)节点,通过这些引用形成逻辑上的连续序列。
2.1.2 链表与数组的核心对比
| 对比维度 | 数组(ArrayList 底层) | 链表 |
|---|---|---|
| 物理存储 | 连续空间 | 非连续空间 |
| 逻辑顺序 | 由物理位置决定 | 由引用链接决定 |
| 元素访问 | 支持随机访问(索引 O (1)) | 不支持(需遍历 O (n)) |
| 插入 / 删除(任意位置) | 需搬移元素(O (n)) | 无需搬移(修改引用 O (1)) |
| 容量管理 | 固定容量,需扩容 | 无固定容量,动态增减节点 |
2.1.3 链表的节点结构
链表的基本组成单位是 "节点"(Node),每个节点通常包含两部分:
- 数据域(data):存储节点数据;
- 引用域(next/pre):存储指向其他节点的引用(指针)。
- 单向链表节点结构:
java
class Node {
int data; // 数据域
Node next; // 引用域(指向后一个节点)
public Node(int data) {
this.data = data;
this.next = null;
}
}
- 双向链表节点结构:
java
class Node {
int data; // 数据域
Node prev; // 引用域(指向前一个节点)
Node next; // 引用域(指向后一个节点)
public Node(int data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
2.2 链表的分类:8 种组合结构
链表结构灵活,根据 3 个核心标准可组合出 8 种结构:
- 按节点链接方向:单向、双向;
- 按是否有头节点:带头、不带头;
- 按是否循环:循环、非循环。
2.2.1 分类详解
-
单向 vs 双向
- 单向链表:每个节点仅一个
next引用,只能从前往后遍历; - 双向链表:每个节点有
prev和next两个引用,可双向遍历。
- 单向链表:每个节点仅一个
-
带头 vs 不带头
- 带头链表:存在专门的 "头节点"(head),不存储实际数据,仅用于指向第一个数据节点,简化操作;
- 不带头链表:无专门头节点,第一个节点即为数据节点,直接通过其引用访问链表。
-
循环 vs 非循环
- 循环链表:最后一个节点的引用指向头节点(或第一个数据节点),形成闭环,遍历可循环;
- 非循环链表:最后一个节点的引用为
null,遍历到尾节点结束。
2.2.2 8 种组合结构
- 单向、带头、循环;
- 单向、带头、非循环;
- 单向、不带头、循环;
- 单向、不带头、非循环;
- 双向、带头、循环;
- 双向、带头、非循环;
- 双向、不带头、循环;
- 双向、不带头、非循环。
2.3 重点掌握的两种链表结构
实际开发和面试中,重点掌握以下两种核心结构:
2.3.1 无头单向非循环链表
- 结构特点:不带头节点、单向链接、非循环(尾节点
next为null); - 优势:结构最简单,实现成本低;
- 适用场景:极少单独存储数据,多作为其他数据结构的子结构(如哈希桶、图的邻接表),是笔试面试高频考点。
示意图:
java
data1 -> data2 -> data3 -> data4 -> null
2.3.2 无头双向循环链表
- 结构特点:不带头节点、双向链接、循环(尾节点
next指向首节点,首节点prev指向尾节点); - 优势:支持双向遍历,插入 / 删除效率更高(无需遍历查找前驱节点);
- 适用场景:Java 集合框架中 LinkedList 的底层实现。
示意图:
java
data1 <-> data2 <-> data3 <-> data4
^ |
| v
+--------------------------+
2.4 链表的核心操作思想
链表的核心操作(插入、删除、查找、遍历)逻辑与数组完全不同,核心是修改节点引用关系,而非搬移元素。
2.4.1 插入操作(单向链表示例)
在节点 A 和 B 之间插入节点 C:
- 找到前驱节点 A;
- C 的
next指向 B; - A 的
next指向 C。
示意图:
java
插入前:A -> B
插入后:A -> C -> B
双向链表插入需额外处理 prev 引用:
- 找到 A(前驱)和 B(后继);
- C 的
prev = A,C 的next = B; - A 的
next = C,B 的prev = C。
2.4.2 删除操作(单向链表示例)
删除节点 B(前驱 A,后继 C):
- 找到前驱节点 A;
- A 的
next直接指向 C; - (Java 中)无需手动释放 B 的内存,垃圾回收机制自动处理。
示意图:
java
删除前:A -> B -> C
删除后:A -> C
双向链表删除需同时处理 prev 和 next:
- 找到 A(前驱)和 C(后继);
- A 的
next = C; - C 的
prev = A。
2.4.3 查找操作
链表不支持随机访问,查找需遍历:
- 从首节点开始;
- 依次对比每个节点的
data与目标值; - 找到返回节点,未找到返回
null。
时间复杂度:O (n)(最坏需遍历所有节点)。
三、无头单向非循环链表的完整实现
无头单向非循环链表是链表的基础,也是面试高频考点。本节将实现其核心操作:头插、尾插、任意位置插入、查找、删除(单个 / 所有目标节点)、清空、遍历等。
3.1 节点类定义
java
class Node {
int data; // 数据域
Node next; // 引用域(指向后一个节点)
// 构造方法:初始化数据
public Node(int data) {
this.data = data;
this.next = null; // 初始 next 为 null
}
}
3.2 链表核心操作实现
java
public class SingleLinkedList {
// 链表头引用(指向第一个数据节点,无头节点)
private Node head;
// 有效元素个数
private int size;
// 构造方法:初始化空链表
public SingleLinkedList() {
this.head = null;
this.size = 0;
}
// 1. 头插法:在链表头部插入元素
public void addFirst(int data) {
Node newNode = new Node(data);
// 新节点 next 指向原头节点
newNode.next = head;
// 更新 head 为新节点
head = newNode;
size++;
}
// 2. 尾插法:在链表尾部插入元素
public void addLast(int data) {
Node newNode = new Node(data);
// 空链表直接让 head 指向新节点
if (head == null) {
head = newNode;
} else {
// 遍历找到尾节点(next 为 null)
Node cur = head;
while (cur.next != null) {
cur = cur.next;
}
// 尾节点 next 指向新节点
cur.next = newNode;
}
size++;
}
// 3. 任意位置插入:第一个数据节点为 0 号下标
public void addIndex(int index, int data) {
// 校验 index 合法性(0 <= index <= size)
if (index < 0 || index > size) {
throw new IllegalArgumentException("Index is illegal: " + index);
}
// index=0 直接头插
if (index == 0) {
addFirst(data);
return;
}
// index=size 直接尾插
if (index == size) {
addLast(data);
return;
}
// 找到 index 的前驱节点(index-1 位置)
Node prev = findNodeByIndex(index - 1);
// 创建新节点并修改引用
Node newNode = new Node(data);
newNode.next = prev.next;
prev.next = newNode;
size++;
}
// 辅助方法:根据索引查找节点(内部使用)
private Node findNodeByIndex(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Index is illegal: " + index);
}
Node cur = head;
// 遍历 index 次找到目标节点
for (int i = 0; i < index; i++) {
cur = cur.next;
}
return cur;
}
// 4. 查找是否包含关键字 key
public boolean contains(int key) {
Node cur = head;
while (cur != null) {
if (cur.data == key) {
return true;
}
cur = cur.next;
}
return false;
}
// 5. 删除第一次出现的关键字 key
public void remove(int key) {
// 空链表直接返回
if (head == null) {
return;
}
// 头节点为目标节点
if (head.data == key) {
head = head.next;
size--;
return;
}
// 找到目标节点的前驱
Node prev = head;
while (prev.next != null) {
if (prev.next.data == key) {
// 跳过目标节点(删除)
prev.next = prev.next.next;
size--;
return;
}
prev = prev.next;
}
// 未找到 key 不操作
}
// 6. 删除所有值为 key 的节点
public void removeAllKey(int key) {
if (head == null) {
return;
}
// 先处理中间和尾节点(跳过头节点)
Node prev = head;
Node cur = head.next;
while (cur != null) {
if (cur.data == key) {
prev.next = cur.next;
cur = cur.next;
size--;
} else {
prev = cur;
cur = cur.next;
}
}
// 最后处理头节点(若头节点为 key)
if (head.data == key) {
head = head.next;
size--;
}
}
// 7. 获取链表长度
public int size() {
return size;
}
// 8. 遍历打印链表
public void display() {
Node cur = head;
while (cur != null) {
System.out.print(cur.data + " ");
cur = cur.next;
}
System.out.println();
}
// 9. 清空链表
public void clear() {
// 方式 1:直接置空 head,GC 回收所有节点
head = null;
size = 0;
// 方式 2:遍历断开所有引用(帮助 GC 快速回收)
/*
Node cur = head;
while (cur != null) {
Node next = cur.next;
cur.next = null;
cur = next;
}
head = null;
size = 0;
*/
}
}
3.3 测试代码
java
public class SingleLinkedListTest {
public static void main(String[] args) {
SingleLinkedList list = new SingleLinkedList();
// 测试尾插
list.addLast(1);
list.addLast(2);
list.addLast(3);
list.addLast(4);
System.out.println("尾插后:");
list.display(); // 输出:1 2 3 4
System.out.println("长度:" + list.size()); // 4
// 测试头插
list.addFirst(0);
System.out.println("头插后:");
list.display(); // 0 1 2 3 4
System.out.println("长度:" + list.size()); // 5
// 测试任意位置插入
list.addIndex(3, 99);
System.out.println("索引 3 插入 99 后:");
list.display(); // 0 1 2 99 3 4
System.out.println("长度:" + list.size()); // 6
// 测试 contains
System.out.println("包含 99?" + list.contains(99)); // true
System.out.println("包含 100?" + list.contains(100)); // false
// 测试 remove(删除第一个 3)
list.remove(3);
System.out.println("删除第一个 3 后:");
list.display(); // 0 1 2 99 4
System.out.println("长度:" + list.size()); // 5
// 测试 removeAllKey(添加重复 4)
list.addLast(4);
list.addLast(4);
System.out.println("添加重复 4 后:");
list.display(); // 0 1 2 99 4 4 4
list.removeAllKey(4);
System.out.println("删除所有 4 后:");
list.display(); // 0 1 2 99
System.out.println("长度:" + list.size()); // 4
// 测试清空
list.clear();
System.out.println("清空后长度:" + list.size()); // 0
list.display(); // 无输出
}
}
3.4 实现要点
- 空链表判断 :所有操作前需判断
head == null,避免空指针; - 索引合法性:插入、查找时校验索引范围,非法则抛异常;
- 头节点特殊处理 :头插、删除头节点直接修改
head引用; - 删除所有节点:先处理中间节点,最后处理头节点,避免漏删;
- 内存释放:Java 无需手动释放节点,断开引用后 GC 自动回收。
四、链表经典面试题解析(10 道核心题)
链表是笔试面试高频考点,以下 10 道题涵盖核心算法思想(快慢指针、虚拟头节点、链表拼接等),每道题含思路、代码、注意事项。
4.1 题目 1:删除链表中等于给定值 val 的所有节点(LeetCode 203)
题目描述
给链表头节点 head 和整数 val,删除所有 Node.val == val 的节点,返回新头节点。
示例
输入:head = [1,2,6,3,4,5,6], val = 6 → 输出:[1,2,3,4,5]
思路
- 难点:头节点可能是目标节点,空链表需特殊处理;
- 解决方案:虚拟头节点 (dummy node),其
next指向原头节点,统一头节点与非头节点的删除逻辑; - 步骤:
- 创建虚拟头节点
dummy,dummy.next = head; - 定义
prev = dummy(前驱)、cur = head(当前); - 遍历链表,
cur.val == val则prev.next = cur.next,否则prev = cur; cur = cur.next,遍历结束返回dummy.next。
- 创建虚拟头节点
代码实现
java
public ListNode removeElements(ListNode head, int val) {
// 虚拟头节点
ListNode dummy = new ListNode(-1);
dummy.next = head;
ListNode prev = dummy;
ListNode cur = head;
while (cur != null) {
if (cur.val == val) {
prev.next = cur.next;
} else {
prev = cur;
}
cur = cur.next;
}
return dummy.next;
}
// 链表节点类(LeetCode 已定义)
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
注意事项
- 虚拟头节点统一删除逻辑,避免单独处理头节点;
- 仅当当前节点不删除时,前驱节点才后移。
4.2 题目 2:反转一个单链表(LeetCode 206)
题目描述
给单链表头节点 head,反转链表并返回新头节点。
示例
输入:[1,2,3,4,5] → 输出:[5,4,3,2,1]
思路(迭代法)
- 核心:双指针 ,修改节点
next引用方向; - 步骤:
prev = null(前驱),cur = head(当前);- 遍历中保存
cur.next(避免反转后丢失); cur.next = prev(反转引用);prev = cur、cur = nextNode(指针后移);- 遍历结束,
prev为新头节点。
代码实现(迭代法)
java
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while (cur != null) {
ListNode nextNode = cur.next; // 保存下一个节点
cur.next = prev; // 反转
prev = cur;
cur = nextNode;
}
return prev;
}
进阶:递归法
java
public ListNode reverseList(ListNode head) {
// 终止条件:空链表或单个节点
if (head == null || head.next == null) {
return head;
}
// 递归反转后续链表
ListNode newHead = reverseList(head.next);
// 反转当前节点与下一个节点
head.next.next = head;
head.next = null; // 避免循环
return newHead;
}
注意事项
- 迭代法时间复杂度 O (n),空间复杂度 O (1)(原地反转);
- 递归法时间复杂度 O (n),空间复杂度 O (n)(递归栈深度);
- 必须先保存
cur.next,否则丢失后续链表。
4.3 题目 3:链表的中间结点(LeetCode 876)
题目描述
给非空单链表,返回中间节点;若有两个中间节点,返回第二个。
示例
输入:[1,2,3,4,5] → 输出:3;输入:[1,2,3,4,5,6] → 输出:4
思路
- 核心:快慢指针,快指针每次走 2 步,慢指针每次走 1 步;
- 原理:快指针遍历到尾时,慢指针恰好到中间:
- 奇数长度:快指针到尾节点(
fast.next == null),慢指针在中间; - 偶数长度:快指针到
null,慢指针在第二个中间节点。
- 奇数长度:快指针到尾节点(
代码实现
java
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
// 循环条件:fast 不为 null 且 fast.next 不为 null
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
注意事项
- 循环条件不能写反(先判断
fast != null),避免空指针; - 时间复杂度 O (n),空间复杂度 O (1)。
4.4 题目 4:链表中倒数第 k 个结点(剑指 Offer 22)
题目描述
输入链表,输出倒数第 k 个节点(从 1 计数,尾节点为倒数第 1 个)。
示例
输入:1→2→3→4→5, k=2 → 输出:4
思路
- 核心:快慢指针,快指针先出发 k 步,再与慢指针同时遍历,快指针到尾时,慢指针即为目标节点;
- 步骤:
- 校验:链表为空或 k≤0 返回 null;
- 快指针先遍历 k 步(若过程中
fast == null,说明 k 大于链表长度,返回 null); - 快慢指针同时遍历,直到
fast == null; - 慢指针即为倒数第 k 个节点。
代码实现
java
public ListNode getKthFromEnd(ListNode head, int k) {
if (head == null || k <= 0) {
return null;
}
ListNode fast = head;
// 快指针先跑 k 步
for (int i = 0; i < k; i++) {
if (fast == null) {
return null; // k 超出链表长度
}
fast = fast.next;
}
// 快慢指针同时跑
ListNode slow = head;
while (fast != null) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
注意事项
- 需校验 k 的合法性和链表长度,避免数组越界;
- 快慢指针间距为 k,快指针到尾时,慢指针恰好指向目标。
4.5 题目 5:合并两个有序链表(LeetCode 21)
题目描述
将两个升序链表合并为新的升序链表,返回新头节点。
示例
输入:l1 = [1,2,4], l2 = [1,3,4] → 输出:[1,1,2,3,4,4]
思路
- 核心:双指针 + 虚拟头节点,类似归并排序的合并过程;
- 步骤:
- 创建虚拟头节点
dummy,current指向dummy; - 双指针
p1、p2分别指向两个链表头; - 比较
p1和p2的值,将较小节点接入current.next,对应指针后移; - 遍历结束后,接入剩余链表;
- 返回
dummy.next。
- 创建虚拟头节点
代码实现
java
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode dummy = new ListNode(-1);
ListNode current = dummy;
ListNode p1 = list1;
ListNode p2 = list2;
while (p1 != null && p2 != null) {
if (p1.val <= p2.val) {
current.next = p1;
p1 = p1.next;
} else {
current.next = p2;
p2 = p2.next;
}
current = current.next;
}
// 接入剩余节点
current.next = (p1 != null) ? p1 : p2;
return dummy.next;
}
进阶:递归法
java
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
if (list1 == null) return list2;
if (list2 == null) return list1;
if (list1.val <= list2.val) {
list1.next = mergeTwoLists(list1.next, list2);
return list1;
} else {
list2.next = mergeTwoLists(list1, list2.next);
return list2;
}
}
注意事项
- 迭代法时间复杂度 O (n+m)(n、m 为链表长度),空间复杂度 O (1);
- 递归法时间复杂度 O (n+m),空间复杂度 O (n+m)(递归栈)。
4.6 题目 6:以给定值 x 为基准分割链表(LeetCode 86)
题目描述
给链表头节点 head 和值 x,分割链表使所有小于 x 的节点在前,大于等于 x 的节点在后,保留节点初始相对位置。
示例
输入:[1,4,3,2,5,2], x=3 → 输出:[1,2,2,4,3,5]
思路
- 核心:拆分链表 + 拼接 ,创建两个虚拟头节点分别存储小于
x和大于等于x的节点,最后拼接; - 步骤:
- 创建
smallDummy(存小于 x)、largeDummy(存大于等于 x); small、large指针分别指向两个虚拟头节点;- 遍历原链表,分配节点到对应链表;
small.next = largeDummy.next(拼接);large.next = null(避免循环);- 返回
smallDummy.next。
- 创建
代码实现
java
public ListNode partition(ListNode head, int x) {
ListNode smallDummy = new ListNode(-1);
ListNode largeDummy = new ListNode(-1);
ListNode small = smallDummy;
ListNode large = largeDummy;
ListNode cur = head;
while (cur != null) {
if (cur.val < x) {
small.next = cur;
small = small.next;
} else {
large.next = cur;
large = large.next;
}
cur = cur.next;
}
// 拼接两个链表
small.next = largeDummy.next;
large.next = null; // 避免循环
return smallDummy.next;
}
注意事项
- 必须将
large.next置为null,否则可能出现链表循环; - 时间复杂度 O (n),空间复杂度 O (1)。
4.7 题目 7:链表的回文结构(剑指 Offer 27)
题目描述
判断链表是否为回文链表(正序与逆序遍历结果一致)。
示例
输入:1→2→2→1 → 输出:true;输入:1→2 → 输出:false
思路
- 核心:快慢指针找中间 + 反转后半部分 + 对比前后;
- 步骤:
- 快慢指针找中间节点;
- 反转后半部分链表;
- 双指针分别从表头和反转后的后半部分表头对比;
- (可选)恢复原链表结构;
- 所有节点相等则为回文。
代码实现
java
public boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}
// 步骤 1:找中间节点
ListNode slow = head;
ListNode fast = head;
while (fast.next != null && fast.next.next != null) {
slow = slow.next;
fast = fast.next.next;
}
ListNode secondHalfHead = slow.next; // 后半部分头节点
// 步骤 2:反转后半部分
ListNode reversedSecondHalf = reverseList(secondHalfHead);
// 步骤 3:对比前后两部分
ListNode p1 = head;
ListNode p2 = reversedSecondHalf;
boolean isPalindrome = true;
while (p2 != null) {
if (p1.val != p2.val) {
isPalindrome = false;
break;
}
p1 = p1.next;
p2 = p2.next;
}
// 步骤 4:恢复原链表(可选)
slow.next = reverseList(reversedSecondHalf);
return isPalindrome;
}
// 辅助方法:反转链表(同题目 2)
private ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode cur = head;
while (cur != null) {
ListNode nextNode = cur.next;
cur.next = prev;
prev = cur;
cur = nextNode;
}
return prev;
}
注意事项
- 找中间节点时,循环条件确保慢指针指向中间(偶数长度指向第一个中间节点);
- 对比时仅需遍历到
p2为null(后半部分长度 ≤ 前半部分); - 恢复原链表是良好编程习惯,避免修改输入数据。
4.8 题目 8:两个链表的第一个公共节点(剑指 Offer 52)
题目描述
输入两个链表,找出第一个公共节点(物理地址相同,非仅值相等)。
示例
输入:链表 A 4→1→8→4→5,链表 B 5→0→1→8→4→5 → 输出:8
思路
- 核心:双指针相遇法,两个指针分别从两个链表头出发,遍历到尾后切换到另一个链表头,直到相遇;
- 原理:设链表 A 长度 m,B 长度 n,公共部分长度 k,则非公共部分长度为 m-k 和 n-k;两指针总遍历长度均为 m+n,最终会在公共节点相遇(无公共节点则都到
null)。
代码实现
java
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA;
ListNode pB = headB;
while (pA != pB) {
// 遍历完一个链表则切换到另一个
pA = (pA == null) ? headB : pA.next;
pB = (pB == null) ? headA : pB.next;
}
return pA;
}
注意事项
- 公共节点是物理地址相同,而非值相等;
- 时间复杂度 O (m+n),空间复杂度 O (1);
- 无公共节点时,两指针最终都为
null,循环退出返回null。
4.9 题目 9:判断链表中是否有环(LeetCode 141)
题目描述
判断链表是否有环(存在节点可通过 next 指针再次到达)。
示例
输入:有环链表 3→2→0→-4→2 → 输出:true;输入:无环链表 1→2 → 输出:false
思路
- 核心:快慢指针,慢指针走 1 步,快指针走 2 步;
- 原理:
- 无环:快指针先到
null,返回 false; - 有环:快指针和慢指针最终在环中相遇(每次距离缩小 1 步,不会套圈)。
- 无环:快指针先到
关键问题解答
-
为什么快指针走 2 步、慢指针走 1 步能相遇?
- 慢指针进环时,快指针已在环中,两者最大距离为环长 R;
- 每次距离缩小 1 步,慢指针走 1 圈前必被追上。
-
快指针走 3 步、4 步可行吗?
- 不可行。例如快指针走 3 步、慢指针走 1 步,可能出现快指针跳过慢指针,永远不相遇。
代码实现
java
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next; // 初始位置避免循环未开始就相等
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
注意事项
- 快慢指针初始位置可设为(head, head)或(head, head.next),前者循环条件为
fast != null && fast.next != null; - 有环必相遇,无环快指针先到
null,不会死循环; - 时间复杂度 O (n),空间复杂度 O (1)。
4.10 题目 10:链表入环的第一个节点(LeetCode 142)
题目描述
给链表头节点 head,返回入环第一个节点;无环返回 null。
示例
输入:有环链表 3→2→0→-4→2 → 输出:2
思路
-
核心:快慢指针相遇 + 双指针相遇,结合数学推导;
-
数学推导:定义:
- H:链表起始点;E:入环点;M:快慢指针相遇点;
- L:H 到 E 的距离;X:E 到 M 的距离;R:环长。
推导:
- 相遇时,慢指针走了 L+X,快指针走了 L+X+nR(n≥1);
- 快指针速度是慢指针 2 倍:2 (L+X) = L+X+nR → L = nR - X;
- 结论:从 H 和 M 出发的指针,每次走 1 步,最终在 E 相遇。
-
步骤:
- 快慢指针判断是否有环,找到相遇点 M;
- 双指针分别从 H(head)和 M 出发,相遇点即为 E。
代码实现
java
public ListNode detectCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
boolean hasCycle = false;
// 步骤 1:判断是否有环,找到相遇点
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
hasCycle = true;
break;
}
}
if (!hasCycle) {
return null;
}
// 步骤 2:双指针找入环点
ListNode p1 = head;
ListNode p2 = slow;
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
}
return p1;
}
注意事项
- 数学推导是核心,需理解 L = nR - X 的含义;
- 当 n=1 时,L = R - X,两指针同时到达 E;
- 时间复杂度 O (n),空间复杂度 O (1),为最优解法。
五、无头双向循环链表的实现(LinkedList 底层模拟)
Java 中 LinkedList 底层是无头双向循环链表,支持双向遍历,插入 / 删除效率更高。本节模拟其核心操作,帮助理解 LinkedList 底层原理。
5.1 节点类定义
java
class ListNode {
int data; // 数据域
ListNode prev; // 前驱引用
ListNode next; // 后继引用
public ListNode(int data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
5.2 链表核心操作实现
java
public class MyLinkedList {
private ListNode head; // 头引用(指向第一个数据节点)
private int size; // 有效元素个数
public MyLinkedList() {
this.head = null;
this.size = 0;
}
// 1. 头插法
public void addFirst(int data) {
ListNode newNode = new ListNode(data);
if (head == null) {
// 空链表,新节点自循环
head = newNode;
newNode.prev = newNode;
newNode.next = newNode;
} else {
ListNode last = head.prev; // 原尾节点(head 的前驱)
// 新节点的前驱指向尾节点,后继指向头节点
newNode.prev = last;
newNode.next = head;
// 尾节点的后继指向新节点,头节点的前驱指向新节点
last.next = newNode;
head.prev = newNode;
// 更新 head 为新节点
head = newNode;
}
size++;
}
// 2. 尾插法
public void addLast(int data) {
ListNode newNode = new ListNode(data);
if (head == null) {
addFirst(data);
return;
}
ListNode last = head.prev; // 原尾节点
// 新节点前驱指向尾节点,后继指向头节点(循环)
newNode.prev = last;
newNode.next = head;
// 尾节点后继指向新节点,头节点前驱指向新节点
last.next = newNode;
head.prev = newNode;
size++;
}
// 3. 任意位置插入
public void addIndex(int index, int data) {
if (index < 0 || index > size) {
throw new IllegalArgumentException("Index is illegal: " + index);
}
if (index == 0) {
addFirst(data);
return;
}
if (index == size) {
addLast(data);
return;
}
// 找到 index 位置的节点
ListNode cur = findNodeByIndex(index);
ListNode prevNode = cur.prev; // 前驱节点
// 创建新节点并修改引用
ListNode newNode = new ListNode(data);
newNode.prev = prevNode;
newNode.next = cur;
prevNode.next = newNode;
cur.prev = newNode;
size++;
}
// 辅助方法:根据索引查找节点(优化遍历方向)
private ListNode findNodeByIndex(int index) {
if (index < 0 || index >= size) {
throw new IllegalArgumentException("Index is illegal: " + index);
}
ListNode cur = head;
// 前半部分:从 head 向后遍历
if (index < size / 2) {
for (int i = 0; i < index; i++) {
cur = cur.next;
}
} else {
// 后半部分:从尾节点向前遍历
for (int i = 0; i < size - index; i++) {
cur = cur.prev;
}
}
return cur;
}
// 4. 查找是否包含 key
public boolean contains(int key) {
if (head == null) {
return false;
}
ListNode cur = head;
// 循环条件:cur != head(双向循环)
do {
if (cur.data == key) {
return true;
}
cur = cur.next;
} while (cur != head);
return false;
}
// 5. 删除第一次出现的 key
public void remove(int key) {
if (head == null) {
return;
}
ListNode cur = head;
do {
if (cur.data == key) {
// 链表只有一个节点
if (size == 1) {
head = null;
} else {
// 删除头节点
if (cur == head) {
head = head.next;
}
// 修改前驱和后继引用
ListNode prevNode = cur.prev;
ListNode nextNode = cur.next;
prevNode.next = nextNode;
nextNode.prev = prevNode;
}
size--;
return;
}
cur = cur.next;
} while (cur != head);
}
// 6. 删除所有值为 key 的节点
public void removeAllKey(int key) {
if (head == null) {
return;
}
ListNode cur = head;
do {
ListNode nextNode = cur.next; // 保存下一个节点
if (cur.data == key) {
if (size == 1) {
head = null;
} else {
if (cur == head) {
head = nextNode;
}
// 修改引用
cur.prev.next = nextNode;
nextNode.prev = cur.prev;
}
size--;
}
cur = nextNode;
} while (head != null && cur != head);
}
// 7. 获取链表长度
public int size() {
return size;
}
// 8. 正向遍历打印
public void display() {
if (head == null) {
System.out.println("链表为空");
return;
}
ListNode cur = head;
do {
System.out.print(cur.data + " ");
cur = cur.next;
} while (cur != head);
System.out.println();
}
// 9. 反向遍历打印
public void displayReverse() {
if (head == null) {
System.out.println("链表为空");
return;
}
ListNode cur = head.prev; // 从尾节点开始
do {
System.out.print(cur.data + " ");
cur = cur.prev;
} while (cur != head.prev);
System.out.println();
}
// 10. 清空链表
public void clear() {
if (head == null) {
return;
}
ListNode cur = head;
do {
ListNode nextNode = cur.next;
// 断开所有引用
cur.prev = null;
cur.next = null;
cur = nextNode;
} while (cur != head);
head = null;
size = 0;
}
}
5.3 测试代码
java
public class MyLinkedListTest {
public static void main(String[] args) {
MyLinkedList list = new MyLinkedList();
// 尾插测试
list.addLast(1);
list.addLast(2);
list.addLast(3);
list.addLast(4);
System.out.println("尾插正向遍历:");
list.display(); // 1 2 3 4
System.out.println("尾插反向遍历:");
list.displayReverse(); // 4 3 2 1
System.out.println("长度:" + list.size()); // 4
// 头插测试
list.addFirst(0);
System.out.println("头插正向遍历:");
list.display(); // 0 1 2 3 4
System.out.println("长度:" + list.size()); // 5
// 任意位置插入测试
list.addIndex(3, 99);
System.out.println("索引 3 插入 99 后:");
list.display(); // 0 1 2 99 3 4
System.out.println("长度:" + list.size()); // 6
// contains 测试
System.out.println("包含 99?" + list.contains(99)); // true
System.out.println("包含 100?" + list.contains(100)); // false
// remove 测试
list.remove(3);
System.out.println("删除第一个 3 后:");
list.display(); // 0 1 2 99 4
System.out.println("长度:" + list.size()); // 5
// removeAllKey 测试
list.addLast(4);
list.addLast(4);
System.out.println("添加重复 4 后:");
list.display(); // 0 1 2 99 4 4 4
list.removeAllKey(4);
System.out.println("删除所有 4 后:");
list.display(); // 0 1 2 99
System.out.println("长度:" + list.size()); // 4
// 清空测试
list.clear();
System.out.println("清空后长度:" + list.size()); // 0
list.display(); // 链表为空
}
}
5.4 双向循环链表的优势与实现要点
优势
- 支持双向遍历,灵活度高;
- 插入 / 删除效率高(O (1)),无需查找前驱节点;
- 循环结构避免
null判断,遍历逻辑简洁。
实现要点
- 维护循环结构:插入 / 删除时确保尾节点
next指向头节点,头节点prev指向尾节点; - 头节点更新:头插或删除头节点时及时更新
head; - 遍历终止条件:
cur != head(而非cur == null); - 索引查找优化:根据索引位置选择正向或反向遍历,减少次数。
六、LinkedList 的使用指南(Java 集合框架)
Java 集合框架中的 LinkedList 基于无头双向循环链表实现,实现了 List、Deque 等接口,提供丰富 API。本节详细介绍其使用方法。
6.1 LinkedList 的类结构与核心特性
类结构
java
java.lang.Object
↳ java.util.AbstractCollection<E>
↳ java.util.AbstractList<E>
↳ java.util.AbstractSequentialList<E>
↳ java.util.LinkedList<E>
实现的核心接口:
- List<E>:提供有序、可重复、支持索引的功能;
- Deque<E>:提供双端队列功能(两端插入 / 删除);
- Cloneable:支持浅克隆;
- Serializable:支持序列化。
核心特性
- 底层:无头双向循环链表;
- 随机访问:不支持(未实现 RandomAccess 接口),索引访问需遍历(O (n));
- 插入 / 删除:任意位置 O (1)(查找位置 O (n),修改引用 O (1));
- 容量:无固定容量,动态增减节点,无需扩容;
- 线程安全:非线程安全,多线程需手动同步(如
Collections.synchronizedList()); - 支持
null元素。
6.2 LinkedList 的构造方法
| 构造方法 | 说明 |
|---|---|
| LinkedList() | 无参构造,创建空链表 |
| LinkedList(Collection<? extends E> c) | 有参构造,用集合 c 的元素初始化 |
使用示例
java
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class LinkedListConstructor {
public static void main(String[] args) {
// 无参构造
List<Integer> list1 = new LinkedList<>();
list1.add(1);
list1.add(2);
System.out.println("无参构造:" + list1); // [1, 2]
// 有参构造(用 ArrayList 初始化)
List<String> arrayList = new ArrayList<>();
arrayList.add("JavaSE");
arrayList.add("JavaWeb");
List<String> list2 = new LinkedList<>(arrayList);
System.out.println("有参构造:" + list2); // [JavaSE, JavaWeb]
}
}
6.3 LinkedList 的核心方法(List 接口)
| 方法 | 说明 | 时间复杂度 |
|---|---|---|
| boolean add(E e) | 尾插 e | O(1) |
| void add(int index, E e) | 索引 index 插入 e | O (n)(查找 index O (n)) |
| boolean addAll(Collection<? extends E> c) | 尾插集合 c 的元素 | O (m)(m 为集合大小) |
| E remove(int index) | 删除 index 位置元素,返回该元素 | O (n)(查找 index O (n)) |
| boolean remove(Object o) | 删除第一个等于 o 的元素 | O (n)(查找 O (n)) |
| E get(int index) | 获取 index 位置元素 | O (n)(查找 O (n)) |
| E set(int index, E e) | 修改 index 位置元素为 e,返回原元素 | O (n)(查找 O (n)) |
| boolean contains(Object o) | 判断是否包含 o | O(n) |
| int indexOf(Object o) | 返回第一个 o 的索引,未找到返回 -1 | O(n) |
| int lastIndexOf(Object o) | 返回最后一个 o 的索引 | O(n) |
| List<E> subList(int fromIndex, int toIndex) | 返回 [fromIndex, toIndex) 子链表(视图) | O(1) |
| void clear() | 清空链表 | O(n) |
| int size() | 返回元素个数 | O(1) |
使用示例
java
import java.util.LinkedList;
import java.util.List;
public class LinkedListListMethods {
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
// 1. add(E e):尾插
list.add(1);
list.add(2);
list.add(3);
System.out.println("尾插后:" + list); // [1, 2, 3]
// 2. add(int index, E e):任意位置插入
list.add(1, 99);
System.out.println("索引 1 插入 99 后:" + list); // [1, 99, 2, 3]
// 3. addAll(Collection):尾插集合
LinkedList<Integer> other = new LinkedList<>();
other.add(4);
other.add(5);
list.addAll(other);
System.out.println("尾插 other 后:" + list); // [1, 99, 2, 3, 4, 5]
// 4. get(int index):获取元素
int elem = list.get(3);
System.out.println("索引 3 的元素:" + elem); // 3
// 5. set(int index, E e):修改元素
int oldElem = list.set(3, 100);
System.out.println("修改索引 3:原元素=" + oldElem + ",链表=" + list); // [1, 99, 2, 100, 4, 5]
// 6. contains(Object o):判断包含
System.out.println("包含 99?" + list.contains(99)); // true
// 7. indexOf/lastIndexOf
list.add(99);
System.out.println("第一个 99 的索引:" + list.indexOf(99)); // 1
System.out.println("最后一个 99 的索引:" + list.lastIndexOf(99)); // 6
// 8. subList:子链表(视图)
List<Integer> sub = list.subList(1, 4);
System.out.println("子链表 [1,4):" + sub); // [99, 2, 100]
// 9. remove:删除
list.remove(1); // 删除索引 1 的 99
list.remove(Integer.valueOf(4)); // 删除元素 4
System.out.println("删除后:" + list); // [1, 2, 100, 5, 99]
// 10. clear:清空
list.clear();
System.out.println("清空后长度:" + list.size()); // 0
}
}
6.4 LinkedList 的双端队列方法(Deque 接口)
LinkedList 实现 Deque 接口,支持双端队列操作(两端插入 / 删除),效率 O (1):
| 方法 | 说明 |
|---|---|
| void addFirst(E e) | 头插 e |
| void addLast(E e) | 尾插 e(同 add (E e)) |
| boolean offerFirst(E e) | 头插 e,成功返回 true |
| boolean offerLast(E e) | 尾插 e,成功返回 true |
| E removeFirst() | 删除头节点,返回该元素(空链表抛异常) |
| E removeLast() | 删除尾节点,返回该元素(空链表抛异常) |
| E pollFirst() | 删除头节点,返回该元素(空链表返回 null) |
| E pollLast() | 删除尾节点,返回该元素(空链表返回 null) |
| E getFirst() | 获取头节点(空链表抛异常) |
| E getLast() | 获取尾节点(空链表抛异常) |
| E peekFirst() | 获取头节点(空链表返回 null) |
| E peekLast() | 获取尾节点(空链表返回 null) |
使用示例
java
import java.util.LinkedList;
import java.util.Deque;
public class LinkedListDequeMethods {
public static void main(String[] args) {
Deque<Integer> deque = new LinkedList<>();
// 头插/尾插
deque.addFirst(1);
deque.addLast(2);
deque.offerFirst(0);
deque.offerLast(3);
System.out.println("双端操作后:" + deque); // [0, 1, 2, 3]
// 获取头/尾节点
System.out.println("头节点:" + deque.getFirst()); // 0
System.out.println("尾节点:" + deque.peekLast()); // 3
// 删除头/尾节点
deque.removeFirst();
deque.pollLast();
System.out.println("删除后:" + deque); // [1, 2]
}
}
6.5 LinkedList 的遍历方式
LinkedList 支持 3 种常用遍历方式:foreach、迭代器、普通 for 循环(不推荐)。
遍历示例
java
import java.util.LinkedList;
import java.util.ListIterator;
public class LinkedListTraversal {
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
// 1. foreach 遍历(推荐,简洁)
System.out.println("foreach 遍历:");
for (int e : list) {
System.out.print(e + " "); // 1 2 3 4
}
System.out.println();
// 2. 迭代器正向遍历
System.out.println("迭代器正向遍历:");
ListIterator<Integer> it = list.listIterator();
while (it.hasNext()) {
System.out.print(it.next() + " "); // 1 2 3 4
}
System.out.println();
// 3. 迭代器反向遍历
System.out.println("迭代器反向遍历:");
ListIterator<Integer> rit = list.listIterator(list.size());
while (rit.hasPrevious()) {
System.out.print(rit.previous() + " "); // 4 3 2 1
}
System.out.println();
// 4. 普通 for 循环(不推荐,每次 get 都遍历)
System.out.println("普通 for 循环:");
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " "); // 1 2 3 4
}
System.out.println();
}
}
注意事项
- 普通 for 循环遍历效率低(每次
get(i)需从表头 / 表尾遍历到索引 i); - 迭代器支持双向遍历,且可通过
add、remove方法修改链表(foreach 不支持)。
七、ArrayList 与 LinkedList 的核心区别与应用场景
7.1 核心区别对比
| 对比维度 | ArrayList | LinkedList |
|---|---|---|
| 底层结构 | 动态数组(连续物理空间) | 无头双向循环链表(非连续空间) |
| 随机访问 | 支持(O (1)) | 不支持(O (n)) |
| 头插 / 头删 | 需搬移元素(O (n)) | 修改引用(O (1)) |
| 中间插入 / 删除 | 需搬移元素(O (n)) | 查找位置(O (n))+ 修改引用(O (1)) |
| 尾插 / 尾删 | 无扩容时 O (1),需扩容时 O (n) | O(1) |
| 容量管理 | 固定容量,需扩容(1.5 倍) | 无容量概念,动态增减节点 |
| 内存占用 | 连续空间,可能有冗余(扩容后未使用空间) | 每个节点需存储 prev/next 引用,内存开销略大 |
| 线程安全 | 非线程安全 | 非线程安全 |
| 实现接口 | List、RandomAccess 等 | List、Deque、Cloneable 等 |
7.2 应用场景选择
优先使用 ArrayList 的场景
- 频繁通过索引访问元素(随机访问);
- 元素数量稳定,插入 / 删除操作少(尤其是中间插入 / 删除);
- 对内存连续性要求高,需减少额外内存开销。
优先使用 LinkedList 的场景
- 频繁进行任意位置插入 / 删除操作(尤其是头插 / 头删);
- 需实现双端队列功能(两端插入 / 删除);
- 元素数量动态变化大,无需考虑扩容。
7.3 常见误区澄清
-
误区 1:LinkedList 插入 / 删除一定比 ArrayList 快?
- 澄清:LinkedList 插入 / 删除的时间复杂度是 "查找位置 O (n) + 修改引用 O (1)",若插入位置靠近两端,效率很高;但如果插入位置在中间(需遍历查找),效率可能不如 ArrayList(尤其是元素数量少时)。
-
误区 2:ArrayList 扩容一定影响性能?
- 澄清:ArrayList 扩容频率低(初始容量 10,每次扩容 1.5 倍),若提前预估元素数量并指定初始容量(
new ArrayList<>(1000)),可避免扩容,性能优异。
- 澄清:ArrayList 扩容频率低(初始容量 10,每次扩容 1.5 倍),若提前预估元素数量并指定初始容量(
-
误区 3:LinkedList 支持随机访问?
- 澄清:LinkedList 未实现 RandomAccess 接口,通过
get(i)访问元素需遍历,不支持随机访问,遍历效率低。
- 澄清:LinkedList 未实现 RandomAccess 接口,通过
八、总结与拓展
8.1 核心知识点总结
- 链表是物理非连续、逻辑连续的数据结构,核心是节点引用关系;
- 重点掌握两种链表结构:无头单向非循环链表(面试高频)、无头双向循环链表(LinkedList 底层);
- 链表面试题核心思想:快慢指针、虚拟头节点、链表拆分 / 拼接、递归;
- LinkedList 是 List 与 Deque 接口的实现类,适用于频繁插入 / 删除场景;
- ArrayList 与 LinkedList 的选择核心是 "随机访问" 与 "插入 / 删除" 的权衡。
8.2 拓展学习建议
- 刷题练习:LeetCode 链表专题(Easy~Medium 难度),重点练习本文 10 道题的变种;
- 源码阅读:阅读 JDK 中 LinkedList 源码,理解其底层实现细节(如节点类、循环结构维护);
- 进阶数据结构:学习基于链表的复杂数据结构(如哈希桶、图的邻接表、LinkedHashMap 底层);
- 线程安全:学习如何保证 LinkedList/ArrayList 线程安全(
Collections.synchronizedList()、CopyOnWriteArrayList等)。
通过本文的学习,相信大家已全面掌握链表与 LinkedList 的核心知识。链表作为基础数据结构,是算法学习的重中之重,建议多动手实现、多刷题巩固,将知识转化为实战能力。

总结
以上就是今天要讲的内容,本文简单记录了java数据结构,仅作为一份简单的笔记使用,大家根据注释理解,您的点赞关注收藏就是对小编最大的鼓励!