我的个人主页
我的专栏:Java-数据结构,希望能帮助到大家!!!点赞❤ 收藏❤
目录
1. Java LinkedList 基础
1.1 LinkedList 简介
1.2 LinkedList 的实现原理
1.3 LinkedList 与 ArrayList 的区别
2. 链表基础
2.1 链表的定义与种类
2.2 单链表与双链表的区别
2.3 循环链表与普通链表
3. Java LinkedList 的常见操作
3.1 添加元素 (add, addFirst, addLast)
3.2 删除元素 (remove, removeFirst, removeLast)
3.3 查找元素 (get, indexOf, contains)
3.4 修改元素
4. Java LinkedList 的高级操作
4.1 迭代器的使用 (Iterator, ListIterator)
4.2 Queue 和 Deque 接口中的操作
4.3 使用 Collections 对 LinkedList 进行排序和操作
5. 链表的基本操作与算法
5.1 单链表的反转
5.2 链表中环的检测
5.3 链表的合并与分割
5.4 链表的删除与插入操作
6. LinkedList 的性能分析与优化
6.1 时间复杂度分析
6.2 内存消耗与管理
6.3 大数据量时的性能优化
7. LinkedList 的应用场景
7.1 实现队列 (Queue)
7.2 实现栈 (Stack)
7.3 使用链表存储数据历史记录
8. 链表的代码实现
8.1 单链表的手动实现
8.2 双链表的手动实现
8.3 循环链表的手动实现
9. 常见问题与调试技巧
9.1 空指针异常的处理
9.2 ConcurrentModificationException 的避免
9.3 常见逻辑错误与解决方法
10. 总结与扩展
10.1 LinkedList 在实际开发中的适用性
10.2 链表的扩展实现 (如跳跃表)
10.3 学习和实践资源推荐
1. Java LinkedList 基础
1.1 LinkedList 简介
LinkedList
是 Java 集合框架中的一个类,它实现了 List
、Deque
和 Queue
接口,底层基于双向链表实现。
LinkedList的底层是双向链表结构,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引⽤将节点连接起来了,因此在在任意位置插⼊或者删除元素时,不需要搬移元素,效率⽐较⾼。
在集合框架中,LinkedList也实现了List接⼝,具体如下:
【说明】
1. LinkedList实现了List接⼝
2. LinkedList的底层使⽤了双向链表
3. LinkedList没有实现RandomAccess接⼝,因此LinkedList不⽀持随机访问
4. LinkedList的任意位置插⼊和删除元素时效率⽐较⾼,时间复杂度为O(1)
5. LinkedList⽐较适合任意位置插⼊的场景
1.2 LinkedList 的实现原理
LinkedList
的底层是一个双向链表,由节点(Node)组成。每个节点包含数据和指向前后节点的引用。
1.3 LinkedList 与 ArrayList 的区别
- 存储结构:
LinkedList
是链表,ArrayList
是动态数组。 - 访问速度:
ArrayList
随机访问快,LinkedList
插入和删除快。 - 内存消耗:
LinkedList
因为维护节点引用,内存开销更大。
代码示例:创建和使用 LinkedList
import java.util.LinkedList;
public class LinkedListExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");
list.addFirst("Start");
list.addLast("End");
System.out.println("LinkedList: " + list);
list.remove("B");
System.out.println("After removing 'B': " + list);
System.out.println("First Element: " + list.getFirst());
System.out.println("Last Element: " + list.getLast());
}
}
2. 链表基础
2.1 链表的定义与种类
链表是一种通过指针将数据节点连接起来的数据结构,链表是⼀种物理存储结构上⾮连续存储结构,数据元素的逻辑顺序是通过链表中的引⽤链接次序实现的 。主要有以下几种:
注意:
1.从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
2.现实中的结点一般都是从堆上申请出来的
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续
- 单链表:每个节点只指向下一个节点。
- 双链表:每个节点有两个指针,分别指向前后节点。
- 带头
- 不带头
- 循环链表:链表尾部的指针指向头部节点,形成一个环。
重点掌握两种
1.⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
2.⽆头双向链表:在Java的集合框架库中LinkedList底层实现就是⽆头双向循环链表。
2.2 单链表与双链表的区别
1.结构定义
单链表是由一系列节点组成的数据结构。每个节点包含两个部分,一个是数据元素本身,另一个是指向下一个节点的指针(引用)。
例如,用Java语言简单定义一个单链表节点类可以是这样:
class ListNode {
int val; // 数据元素,这里以整数为例
ListNode next; // 指向下一个节点的引用
ListNode(int val) {
this.val = val;
this.next = null;
}
}
整个单链表就像是一条单向的链条,从表头开始,通过每个节点的next
指针依次访问下一个节点,直到最后一个节点的next
指针为null
,表示链表的末尾。
- 双链表的节点除了包含数据元素和指向下一个节点的指针外,还包含一个指向前一个节点的指针。
- 以下是用Java定义双链表节点类的示例:
class DoubleListNode {
int val;
DoubleListNode prev;
DoubleListNode next;
DoubleListNode(int val) {
this.val = val;
this.prev = null;
this.next = null;
}
}
- 双链表就像一个双向的通道,既可以从表头顺着`next`指针向后遍历,也可以从表尾顺着`prev`指针向前遍历。
- 遍历方式
- 单链表:
- 只能单向遍历,从链表的头部开始,通过当前节点的
next
指针逐个访问下一个节点,直到到达链表尾部。 - 例如,在Java中遍历一个单链表可以这样做:
- 只能单向遍历,从链表的头部开始,通过当前节点的
- 单链表:
ListNode head = new ListNode(1);
// 假设已经构建好链表,这里简单示例添加一个节点
ListNode second = new ListNode(2);
head.next = second;
ListNode current = head;
while (current!= null) {
System.out.println(current.val);
current = current.next;
}
双链表:
- 可以双向遍历。既可以从头部开始,通过`next`指针向后遍历,也可以从尾部开始,通过`prev`指针向前遍历。
- 例如,遍历双链表向后的代码如下:
DoubleListNode headDouble = new DoubleListNode(1);
DoubleListNode secondDouble = new DoubleListNode(2);
headDouble.next = secondDouble;
secondDouble.prev = headDouble;
DoubleListNode currentDouble = headDouble;
while (currentDouble!= null) {
System.out.println(currentDouble.val);
currentDouble = currentDouble.next;
}
- 向前遍历的代码如下:
DoubleListNode tail = secondDouble;
while (tail!= null) {
System.out.println(tail.val);
tail = tail.prev;
}
- 插入操作
- 单链表:
- 在指定节点之后插入新节点相对简单。假设要在节点
p
之后插入节点newNode
,步骤如下: - 首先将
newNode
的next
指针指向p
节点的下一个节点(即newNode.next = p.next
),然后将p
节点的next
指针指向newNode
(即p.next = newNode
)。 - 例如:
- 在指定节点之后插入新节点相对简单。假设要在节点
- 单链表:
// 在单链表节点p之后插入newNode
ListNode p = head;
ListNode newNode = new ListNode(3);
newNode.next = p.next;
p.next = newNode;
但是如果要在指定节点之前插入新节点,需要先遍历链表找到该节点的前驱节点,这会比较复杂,尤其是在没有额外的头节点辅助的情况下。
- 双链表:
- 在指定节点之后插入新节点的操作和单链表类似,但是由于有
prev
指针,操作更加方便。在节点p
之后插入newNode
,步骤如下:- 首先将
newNode
的next
指针指向p
节点的下一个节点(即newNode.next = p.next
),然后将p
节点的下一个节点(假设为q
)的prev
指针指向newNode
(即q.prev = newNode
),接着将p
节点的next
指针指向newNode
(即p.next = newNode
),最后将newNode
的prev
指针指向p
(即newNode.prev = p
)。- 在指定节点之前插入新节点也比较方便,因为可以直接通过
prev
指针找到前驱节点进行操作。
-
删除操作
- 单链表:
- 要删除指定节点
p
,需要先找到p
的前驱节点(假设为q
),然后将q
的next
指针指向p
的下一个节点(即q.next = p.next
)。 - 如果没有额外的头节点辅助,删除表头节点时需要特殊处理,因为表头节点没有前驱节点。
- 要删除指定节点
- 双链表:
- 要删除指定节点
p
,可以直接通过p
的prev
指针找到前驱节点(假设为q
),将q
的next
指针指向p
的下一个节点(即q.next = p.next
),同时通过p
的next
指针找到后继节点(假设为r
),将r
的prev
指针指向q
(即r.prev = q
)。 - 无论是删除表头、表尾还是中间节点,操作相对单链表更加统一和方便。
- 要删除指定节点
2.3 循环链表与普通链表
- 单链表:
普通链表由节点依次连接而成,有明确的头节点,最后一个节点的指针指向空,以此标识链表的结尾。例如单链表通过各节点的指针顺序串联数据。而循环链表则是一种特殊形式,它的最后一个节点指针并非指向空,而是指向头节点,从而形成一个闭环结构。在遍历方面,普通链表遇空指针停止,循环链表则需特定条件控制遍历次数以避免死循环。插入操作时,普通链表需关注前驱节点处理,循环链表除插入逻辑外,还得维护循环特性,二者在不同应用场景下各有优势。
3. Java LinkedList 的常见操作
3.1 添加元素
LinkedList
提供了多种方法来添加元素,常用的方法包括:
- add(E element): 在链表末尾添加元素。
- add(int index, E element): 在指定索引处添加元素。
- addFirst(E element): 在链表头部添加元素。
- addLast(E element): 在链表尾部添加元素。
代码示例:添加元素
import java.util.LinkedList;
public class LinkedListAddExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("A"); // 添加到尾部
list.addFirst("Start"); // 添加到头部
list.addLast("End"); // 添加到尾部
list.add(1, "Middle"); // 指定位置添加
System.out.println("LinkedList: " + list);
}
}
3.2 删除元素
LinkedList
提供的方法来删除元素:
- remove(): 移除头部的元素。
- remove(int index): 移除指定位置的元素。
- remove(Object o): 移除第一次出现的指定元素。
- removeFirst(): 移除链表头部的元素。
- removeLast(): 移除链表尾部的元素。
代码示例:删除元素
import java.util.LinkedList;
public class LinkedListRemoveExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("D");
System.out.println("Original List: " + list);
list.remove("B"); // 删除元素 B
System.out.println("After removing 'B': " + list);
list.remove(1); // 删除索引为 1 的元素
System.out.println("After removing index 1: " + list);
list.removeFirst(); // 删除头部元素
System.out.println("After removing first: " + list);
list.removeLast(); // 删除尾部元素
System.out.println("After removing last: " + list);
}
}
3.3 查找元素
LinkedList
提供的方法来查找元素:
- get(int index): 获取指定索引位置的元素。
- getFirst(): 获取链表头部的元素。
- getLast(): 获取链表尾部的元素。
- indexOf(Object o): 返回第一次出现的元素的索引。
- lastIndexOf(Object o): 返回最后一次出现的元素的索引。
- contains(Object o): 检查链表中是否包含指定元素。
代码示例:查找元素
import java.util.LinkedList;
public class LinkedListFindExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");
list.add("B");
System.out.println("LinkedList: " + list);
System.out.println("Element at index 1: " + list.get(1)); // 获取索引为1的元素
System.out.println("First Element: " + list.getFirst()); // 获取头部元素
System.out.println("Last Element: " + list.getLast()); // 获取尾部元素
System.out.println("Index of 'B': " + list.indexOf("B")); // 查找 B 的第一次出现位置
System.out.println("Last Index of 'B': " + list.lastIndexOf("B")); // 查找 B 的最后一次出现位置
System.out.println("Contains 'C': " + list.contains("C")); // 检查是否包含 C
}
}
3.4 修改元素
修改元素通常使用 set(int index, E element)
方法,可以替换指定索引处的元素。
代码示例:修改元素
import java.util.LinkedList;
public class LinkedListModifyExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");
System.out.println("Original List: " + list);
list.set(1, "X"); // 将索引为1的元素替换为 "X"
System.out.println("After modification: " + list);
}
}
3.5 遍历元素
LinkedList
的遍历可以使用 for
循环、增强 for
循环以及迭代器。
代码示例:遍历元素
import java.util.LinkedList;
import java.util.Iterator;
public class LinkedListTraverseExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");
// 使用 for 循环
System.out.println("Using for loop:");
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
System.out.println();
// 使用增强的 for 循环
System.out.println("Using enhanced for loop:");
for (String element : list) {
System.out.print(element + " ");
}
System.out.println();
// 使用迭代器
System.out.println("Using iterator:");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
System.out.println();
}
}
4. Java LinkedList 的高级操作
4.1 迭代器的使用
LinkedList
支持两种迭代器:
- Iterator:用于单向遍历。
- ListIterator:用于双向遍历,并支持在遍历时添加或修改元素。
代码示例:使用 Iterator 和 ListIterator
import java.util.LinkedList;
import java.util.Iterator;
import java.util.ListIterator;
public class LinkedListIteratorExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");
// 使用 Iterator 单向遍历
System.out.println("Using Iterator:");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " ");
}
System.out.println();
// 使用 ListIterator 双向遍历
System.out.println("Using ListIterator (forward):");
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
System.out.print(listIterator.next() + " ");
}
System.out.println();
System.out.println("Using ListIterator (backward):");
while (listIterator.hasPrevious()) {
System.out.print(listIterator.previous() + " ");
}
System.out.println();
}
}
4.2 Queue 和 Deque 接口中的操作
LinkedList
实现了 Deque
接口,因此它可以作为双端队列使用。
常用操作:
-
队列操作:
offer(E e)
:将元素添加到队列尾部。poll()
:移除并返回队列头部的元素。peek()
:返回队列头部的元素但不移除。
-
双端队列操作:
offerFirst(E e)
:在头部添加元素。offerLast(E e)
:在尾部添加元素。pollFirst()
:移除并返回头部的元素。pollLast()
:移除并返回尾部的元素。
代码示例:作为队列使用
import java.util.LinkedList;
import java.util.Queue;
public class LinkedListQueueExample {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
// 添加元素
queue.offer(1);
queue.offer(2);
queue.offer(3);
System.out.println("Queue: " + queue);
// 查看头部元素
System.out.println("Peek: " + queue.peek());
// 移除头部元素
System.out.println("Poll: " + queue.poll());
System.out.println("Queue after poll: " + queue);
}
}
代码示例:作为双端队列使用
import java.util.Deque;
import java.util.LinkedList;
public class LinkedListDequeExample {
public static void main(String[] args) {
Deque<String> deque = new LinkedList<>();
// 双端操作
deque.offerFirst("A");
deque.offerLast("B");
deque.offerFirst("C");
System.out.println("Deque: " + deque);
// 头部和尾部操作
System.out.println("Poll First: " + deque.pollFirst());
System.out.println("Poll Last: " + deque.pollLast());
System.out.println("Deque after poll: " + deque);
}
}
4.3 使用 Collections 对 LinkedList 进行排序和操作
LinkedList
可以使用 Collections
工具类进行排序、反转等操作。
代码示例:对 LinkedList 排序
import java.util.LinkedList;
import java.util.Collections;
public class LinkedListSortExample {
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(5);
list.add(2);
list.add(8);
list.add(1);
System.out.println("Original List: " + list);
// 排序
Collections.sort(list);
System.out.println("Sorted List: " + list);
// 反转
Collections.reverse(list);
System.out.println("Reversed List: " + list);
// 随机打乱
Collections.shuffle(list);
System.out.println("Shuffled List: " + list);
}
}
4.4 克隆 LinkedList
使用 clone()
方法可以创建 LinkedList 的浅拷贝。
代码示例:克隆 LinkedList
import java.util.LinkedList;
public class LinkedListCloneExample {
public static void main(String[] args) {
LinkedList<String> original = new LinkedList<>();
original.add("A");
original.add("B");
original.add("C");
LinkedList<String> cloned = (LinkedList<String>) original.clone();
System.out.println("Original List: " + original);
System.out.println("Cloned List: " + cloned);
// 修改克隆的列表不会影响原列表
cloned.add("D");
System.out.println("Modified Cloned List: " + cloned);
System.out.println("Original List after modification: " + original);
}
}
4.5 将 LinkedList 转为其他集合类型
LinkedList
可以轻松地转为 ArrayList
或其他集合类型。
代码示例:转为 ArrayList
import java.util.LinkedList;
import java.util.ArrayList;
public class LinkedListToArrayListExample {
public static void main(String[] args) {
LinkedList<String> linkedList = new LinkedList<>();
linkedList.add("A");
linkedList.add("B");
linkedList.add("C");
// 转为 ArrayList
ArrayList<String> arrayList = new ArrayList<>(linkedList);
System.out.println("LinkedList: " + linkedList);
System.out.println("ArrayList: " + arrayList);
}
}
5. 链表的基本操作与算法
5.1 单链表的反转
反转单链表的核心思想是改变节点的 next
指针的指向。
代码示例:反转单链表
public class ReverseLinkedList {
static class Node {
int data;
Node next;
Node(int data) {
this.data = data;
}
}
public static Node reverse(Node head) {
Node prev = null;
Node current = head;
while (current != null) {
Node next = current.next;
current.next = prev;
prev = current;
current = next;
}
return prev;
}
public static void printList(Node head) {
Node temp = head;
while (temp != null) {
System.out.print(temp.data + " -> ");
temp = temp.next;
}
System.out.println("null");
}
public static void main(String[] args) {
Node head = new Node(1);
head.next = new Node(2);
head.next.next = new Node(3);
System.out.println("Original List:");
printList(head);
head = reverse(head);
System.out.println("Reversed List:");
printList(head);
}
}
5.2 链表中环的检测
使用快慢指针(Floyd 判圈法)检测链表中是否存在环。
代码示例:检测环
public class DetectCycle {
static class Node {
int data;
Node next;
Node(int data) {
this.data = data;
}
}
public static boolean hasCycle(Node head) {
Node slow = head;
Node fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) {
return true;
}
}
return false;
}
public static void main(String[] args) {
Node head = new Node(1);
head.next = new Node(2);
head.next.next = new Node(3);
head.next.next.next = head; // Creates a cycle
System.out.println("Has Cycle: " + hasCycle(head));
}
}
6. LinkedList 的性能分析与优化
6.1 时间复杂度分析
LinkedList
的操作性能受链表结构的限制,具体复杂度如下:
操作 | 时间复杂度 | 说明 |
---|---|---|
添加元素 | ||
- 头部插入 | O(1) | 直接修改头节点指针即可。 |
- 尾部插入 | O(1) | 如果有尾指针,直接修改尾节点指针;否则需要遍历到尾部。 |
- 指定位置插入 | O(n) | 需要遍历链表找到指定位置。 |
删除元素 | ||
- 头部删除 | O(1) | 修改头节点指针即可。 |
- 尾部删除 | O(n) | 需要遍历找到尾部的前一个节点。 |
- 指定位置删除 | O(n) | 需要遍历找到该位置。 |
访问元素 | O(n) | 需要从头开始逐个遍历找到指定位置。 |
搜索元素 | O(n) | 需要从头到尾遍历链表。 |
总结:
对于频繁的插入、删除操作(尤其是头部或尾部操作),LinkedList
性能优于ArrayList
。
对于频繁的随机访问或搜索操作,ArrayList
是更优选择。
6.2 内存消耗分析
LinkedList
的内存使用较高,因为每个节点不仅存储数据,还需要额外存储前驱和后继的指针。
特性 | LinkedList | ArrayList |
---|---|---|
存储结构 | 双向链表 | 动态数组 |
节点开销 | 每个节点有两个额外指针 | 仅存储数据 |
空间利用率 | 较低 | 较高 |
示例:分析内存消耗
import java.util.LinkedList;
import java.util.ArrayList;
public class MemoryUsageExample {
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
ArrayList<Integer> arrayList = new ArrayList<>();
for (int i = 0; i < 1000; i++) {
linkedList.add(i);
arrayList.add(i);
}
System.out.println("LinkedList size: " + linkedList.size());
System.out.println("ArrayList size: " + arrayList.size());
// 虽然无法直接显示内存使用,但 LinkedList 的开销更高。
}
}
6.3 性能优化方法
-
选择适合的场景:
- 使用
LinkedList
时应避免频繁的随机访问操作。 - 如果操作多为插入、删除,尤其是在头部或尾部,优先选择
LinkedList
。
- 使用
-
减少遍历操作:
- 避免多次调用
get(index)
来访问元素,可以使用Iterator
或增强的for
循环提高效率。
示例:用迭代器代替索引遍历
import java.util.LinkedList; import java.util.Iterator; public class OptimizedTraversal { public static void main(String[] args) { LinkedList<Integer> list = new LinkedList<>(); for (int i = 0; i < 1000; i++) { list.add(i); } // 遍历方式优化 Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { Integer value = iterator.next(); // 处理数据 } } }
- 避免多次调用
-
预估链表大小:
- 如果使用
LinkedList
构建大规模数据结构,尽量减少动态扩展,预先分配节点空间。 - 在某些场景中,可以考虑手动实现链表来减少不必要的开销。
- 如果使用
-
结合其他数据结构:
- 如果随机访问和插入删除操作都需要较高效率,可以结合
HashMap
和LinkedList
,例如实现 LRU 缓存。
- 如果随机访问和插入删除操作都需要较高效率,可以结合
6.4 性能测试与比较
代码示例:LinkedList 与 ArrayList 性能对比
import java.util.LinkedList;
import java.util.ArrayList;
public class PerformanceTest {
public static void main(String[] args) {
LinkedList<Integer> linkedList = new LinkedList<>();
ArrayList<Integer> arrayList = new ArrayList<>();
long startTime, endTime;
// 测试插入性能
startTime = System.nanoTime();
for (int i = 0; i < 100000; i++) {
linkedList.add(0, i);
}
endTime = System.nanoTime();
System.out.println("LinkedList insertion time: " + (endTime - startTime) + " ns");
startTime = System.nanoTime();
for (int i = 0; i < 100000; i++) {
arrayList.add(0, i);
}
endTime = System.nanoTime();
System.out.println("ArrayList insertion time: " + (endTime - startTime) + " ns");
// 测试访问性能
startTime = System.nanoTime();
for (int i = 0; i < linkedList.size(); i++) {
linkedList.get(i);
}
endTime = System.nanoTime();
System.out.println("LinkedList access time: " + (endTime - startTime) + " ns");
startTime = System.nanoTime();
for (int i = 0; i < arrayList.size(); i++) {
arrayList.get(i);
}
endTime = System.nanoTime();
System.out.println("ArrayList access time: " + (endTime - startTime) + " ns");
}
}
测试结果:
- 插入操作:
LinkedList
优于ArrayList
,尤其是在头部插入时。 - 访问操作:
ArrayList
明显优于LinkedList
,因为它支持随机访问。
7. LinkedList 的应用场景
LinkedList
是基于双向链表实现的,适用于某些特定的应用场景。以下列出 LinkedList
的主要应用及实现方法。
7.1 实现队列 (Queue)
LinkedList
实现了 Queue
接口,因此可以直接作为队列使用,支持先进先出的操作。
常用方法:
- offer(E e):在队列尾部添加元素。
- poll():移除并返回队列头部的元素。
- peek():返回队列头部的元素但不移除。
代码示例:实现队列
import java.util.LinkedList;
import java.util.Queue;
public class LinkedListQueueExample {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
// 添加元素到队列
queue.offer(10);
queue.offer(20);
queue.offer(30);
System.out.println("Queue: " + queue);
// 获取并移除头部元素
System.out.println("Poll: " + queue.poll());
// 获取头部元素但不移除
System.out.println("Peek: " + queue.peek());
System.out.println("Queue after operations: " + queue);
}
}
7.2 实现栈 (Stack)
LinkedList
也可以用作栈的实现,支持后进先出的操作。可以使用以下方法:
- push(E e):将元素压入栈顶。
- pop():移除并返回栈顶元素。
- peek():返回栈顶元素但不移除。
代码示例:实现栈
import java.util.LinkedList;
public class LinkedListStackExample {
public static void main(String[] args) {
LinkedList<Integer> stack = new LinkedList<>();
// 压栈
stack.push(10);
stack.push(20);
stack.push(30);
System.out.println("Stack: " + stack);
// 弹栈
System.out.println("Pop: " + stack.pop());
// 查看栈顶元素
System.out.println("Peek: " + stack.peek());
System.out.println("Stack after operations: " + stack);
}
}
7.3 数据历史记录的存储
LinkedList
的双向链表特性,支持快速遍历前后节点,因此适用于实现历史记录功能,如浏览器的前进和后退。
代码示例:实现简单的历史记录功能
import java.util.LinkedList;
public class BrowserHistory {
private LinkedList<String> history = new LinkedList<>();
private int current = -1;
public void visit(String url) {
while (history.size() > current + 1) {
history.removeLast(); // 清除前进历史
}
history.add(url);
current++;
System.out.println("Visited: " + url);
}
public void back() {
if (current > 0) {
current--;
System.out.println("Back to: " + history.get(current));
} else {
System.out.println("No more back history.");
}
}
public void forward() {
if (current < history.size() - 1) {
current++;
System.out.println("Forward to: " + history.get(current));
} else {
System.out.println("No more forward history.");
}
}
public static void main(String[] args) {
BrowserHistory browser = new BrowserHistory();
browser.visit("google.com");
browser.visit("youtube.com");
browser.back();
browser.visit("github.com");
browser.back();
browser.forward();
}
}
8. 链表的代码实现
8.1 单链表的手动实现
代码示例:实现单链表
class MyLinkedList {
private Node head;
private static class Node {
int data;
Node next;
Node(int data) {
this.data = data;
}
}
public void add(int data) {
if (head == null) {
head = new Node(data);
return;
}
Node temp = head;
while (temp.next != null) {
temp = temp.next;
}
temp.next = new Node(data);
}
public void printList() {
Node temp = head;
while (temp != null) {
System.out.print(temp.data + " -> ");
temp = temp.next;
}
System.out.println("null");
}
public static void main(String[] args) {
MyLinkedList list = new MyLinkedList();
list.add(1);
list.add(2);
list.add(3);
list.printList();
}
}
9. 常见问题与调试技巧
9.1 空指针异常(NullPointerException)
问题描述:
当操作一个空的 LinkedList
时(例如访问头部或尾部元素),会抛出 NullPointerException
。
示例代码:
import java.util.LinkedList;
public class NullPointerExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
// 空列表访问会导致 NullPointerException
System.out.println(list.getFirst());
}
}
解决方法:
- 在操作之前检查列表是否为空:
if (!list.isEmpty()) { System.out.println(list.getFirst()); } else { System.out.println("List is empty."); }
- 使用带有默认返回值的方法(如
peekFirst
和peekLast
),这些方法不会抛出异常:System.out.println(list.peekFirst()); // 返回 null 而非抛出异常
9.2 并发修改异常(ConcurrentModificationException)
问题描述:
在遍历 LinkedList
的同时修改其内容(例如添加或删除元素),会抛出 ConcurrentModificationException
。
示例代码:
import java.util.LinkedList;
public class ConcurrentModificationExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("A");
list.add("B");
list.add("C");
for (String element : list) {
if (element.equals("B")) {
list.remove(element); // 在遍历时修改列表
}
}
}
}
解决方法:
- 使用 迭代器 的
remove()
方法:import java.util.Iterator; Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String element = iterator.next(); if (element.equals("B")) { iterator.remove(); // 使用迭代器的 remove 方法 } }
- 使用
CopyOnWriteArrayList
或其他线程安全的集合来避免并发问题。
9.3 性能问题
问题描述:
对 LinkedList
进行频繁的随机访问或搜索操作时,性能可能会很低。
示例代码:
LinkedList<Integer> list = new LinkedList<>();
for (int i = 0; i < 100000; i++) {
list.add(i);
}
// 随机访问
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i)); // 低效:每次 get 都需要从头遍历
}
解决方法:
- 遍历操作时使用迭代器代替随机访问。
- 如果需要频繁随机访问,考虑使用
ArrayList
替代LinkedList
。
9.4 死循环问题
问题描述:
在手动实现链表时,可能会不小心创建一个环,导致死循环。
示例代码:
class Node {
int data;
Node next;
Node(int data) {
this.data = data;
}
}
public class InfiniteLoopExample {
public static void main(String[] args) {
Node head = new Node(1);
Node second = new Node(2);
head.next = second;
second.next = head; // 创建一个循环
Node current = head;
while (current != null) {
System.out.println(current.data);
current = current.next; // 无限循环
}
}
}
解决方法:
- 使用快慢指针检测环:
public static boolean hasCycle(Node head) { Node slow = head; Node fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { return true; // 检测到环 } } return false; }
- 小心修改链表的
next
指针,避免手动制造环。
9.5 IndexOutOfBoundsException
问题描述:
在尝试访问 LinkedList
中不存在的索引时,会抛出 IndexOutOfBoundsException
。
示例代码:
import java.util.LinkedList;
public class IndexOutOfBoundsExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("A");
System.out.println(list.get(5)); // 超出范围
}
}
解决方法:
- 确保访问的索引有效:
if (index >= 0 && index < list.size()) { System.out.println(list.get(index)); } else { System.out.println("Index out of bounds."); }
- 使用异常捕获机制:
try { System.out.println(list.get(5)); } catch (IndexOutOfBoundsException e) { System.out.println("Invalid index: " + e.getMessage()); }
9.6 内存泄漏问题
问题描述:
长时间持有链表的引用,或者链表节点之间存在循环引用,可能导致内存泄漏。
解决方法:
- 使用弱引用(
WeakReference
)来避免不必要的对象持有。 - 手动清理链表不再需要的节点:
list.clear(); // 释放所有元素
9.7 链表的深拷贝问题
问题描述:
默认的 clone()
方法只进行浅拷贝,对于复杂对象可能会引发问题。
示例代码:
LinkedList<Node> list = new LinkedList<>();
list.add(new Node(1));
LinkedList<Node> clonedList = (LinkedList<Node>) list.clone();
clonedList.get(0).data = 100; // 修改克隆后的数据会影响原链表
解决方法:
- 实现深拷贝:
LinkedList<Node> deepClone = new LinkedList<>(); for (Node node : list) { deepClone.add(new Node(node.data)); // 手动复制节点 }
10. 总结与扩展
10.1 LinkedList 在实际开发中的适用性
-
优点:
- 高效的插入和删除操作:
- 在头部、尾部插入或删除元素效率极高,时间复杂度为 O(1)。
- 双向链表的灵活性:
- 可以作为队列(
Queue
)或双端队列(Deque
)使用。
- 可以作为队列(
- 动态扩展性:
- 无需考虑容量问题,链表节点可以动态分配。
- 高效的插入和删除操作:
-
缺点:
- 随机访问性能较差:
- 随机访问需要从头遍历,时间复杂度为 O(n)。
- 内存开销大:
- 每个节点额外存储两个指针(前驱和后继)。
- 随机访问性能较差:
-
适用场景:
- 数据量较小且插入、删除操作频繁。
- 需要使用队列或双端队列功能。
- 需要频繁改变数据结构,例如在中间插入或删除节点。
10.2 链表的扩展实现
-
实现栈(Stack)功能
LinkedList
可以很方便地实现栈,利用addFirst()
和removeFirst()
模拟栈的push
和pop
操作。
代码示例:使用 LinkedList 实现栈
import java.util.LinkedList; public class LinkedListStack { private LinkedList<Integer> stack = new LinkedList<>(); public void push(int value) { stack.addFirst(value); } public int pop() { return stack.removeFirst(); } public int peek() { return stack.getFirst(); } public boolean isEmpty() { return stack.isEmpty(); } public static void main(String[] args) { LinkedListStack stack = new LinkedListStack(); stack.push(10); stack.push(20); System.out.println(stack.pop()); // 输出 20 System.out.println(stack.peek()); // 输出 10 } }
-
实现队列(Queue)功能
LinkedList
本身实现了Queue
接口,天然适合用于队列操作。
-
实现 LRU 缓存(最近最少使用缓存)
- 利用
LinkedHashMap
和LinkedList
的组合,可以实现一个简单的 LRU 缓存。
代码示例:LRU 缓存
import java.util.LinkedHashMap; import java.util.Map; public class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int capacity; public LRUCache(int capacity) { super(capacity, 0.75f, true); this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } public static void main(String[] args) { LRUCache<Integer, String> cache = new LRUCache<>(3); cache.put(1, "A"); cache.put(2, "B"); cache.put(3, "C"); cache.get(1); // 使用键 1 cache.put(4, "D"); // 淘汰键 2 System.out.println(cache); // 输出 {3=C, 1=A, 4=D} } }
- 利用
-
跳表(Skip List)
- 跳表是一种基于链表的扩展数据结构,支持快速插入、删除和搜索,时间复杂度为 O(log n)。
- 可用于实现有序的键值存储。
10.3 学习和实践资源推荐
-
官方文档:
- Java LinkedList 文档
- 深入理解
LinkedList
提供的所有方法及其实现原理。
-
经典书籍:
- 《数据结构与算法分析》:理解链表的基本原理和应用。
- 《Java 编程思想》:深入理解
LinkedList
在集合框架中的地位。
-
开源项目:
- Github 上的
LRU Cache
实现。 - 使用链表构建的自定义数据结构(如双端队列、循环链表)。
- Github 上的
-
实践题目:
- 在在线平台(如 LeetCode、HackerRank)上练习与链表相关的算法题,例如:
- 反转链表
- 合并两个有序链表
- 检测链表中的环
- 在在线平台(如 LeetCode、HackerRank)上练习与链表相关的算法题,例如: