一、快递分拣中心的神奇传送带:LinkedList 的基本结构
想象你来到一个高科技快递分拣中心,这里的包裹传送带不像普通仓库那样排成直线,而是每个包裹都有两个 "小助手"------ 前面的助手指向下一个包裹,后面的助手指向前一个包裹。这就是 Java 中的 LinkedList
,一个由双向链表构成的智能包裹管理系统。
1.1 包裹节点的秘密
每个包裹(节点)都有三个关键信息:
-
包裹里的物品(
item
) -
指向后面包裹的指针(
next
) -
指向前面包裹的指针(
prev
)
java
kotlin
// 快递包裹节点类
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;
}
}
1.2 分拣中心的核心控制室
分拣中心有三个重要的控制变量:
-
first
:第一个包裹的位置 -
last
:最后一个包裹的位置 -
size
:当前包裹的数量
java
java
transient Node<E> first; // 第一个包裹
transient Node<E> last; // 最后一个包裹
transient int size = 0; // 包裹数量
二、包裹入库:LinkedList 的添加操作
2.1 尾部入库:最常用的包裹存放方式
当新包裹到达时,最常见的操作是直接放在传送带尾部。就像分拣中心的传送带会自动把新包裹接在最后一个位置:
java
ini
public boolean add(E e) {
linkLast(e); // 调用尾部添加方法
return true;
}
void linkLast(E e) {
Node<E> l = last; // 获取当前最后一个包裹
Node<E> newNode = new Node<>(l, e, null); // 新建包裹,前一个是原最后一个,后一个为空
last = newNode; // 更新最后一个包裹
if (l == null) {
first = newNode; // 如果原本没有包裹,新包裹也是第一个
} else {
l.next = newNode; // 原最后一个包裹的下一个指向新包裹
}
size++; // 包裹数量加1
}
2.2 插队入库:在指定位置插入包裹
有时需要把包裹插入到特定位置,比如在第 3 个包裹后面插入新包裹,分拣中心会启动智能定位系统:
java
perl
public void add(int index, E element) {
checkPositionIndex(index); // 检查位置是否合法
if (index == size) {
linkLast(element); // 如果是尾部,直接调用尾部添加
} else {
linkBefore(element, node(index)); // 否则找到指定位置并插入
}
}
Node<E> node(int 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;
}
}
三、包裹出库:LinkedList 的删除操作
3.1 紧急出库:删除第一个包裹
当有紧急包裹需要优先处理时,分拣中心会直接取出第一个包裹:
java
ini
public E remove() {
return removeFirst(); // 调用删除第一个包裹方法
}
public E removeFirst() {
Node<E> f = first; // 获取第一个包裹
if (f == null) throw new NoSuchElementException(); // 如果没有包裹,报错
return unlinkFirst(f); // 执行删除
}
private E unlinkFirst(Node<E> f) {
E element = f.item; // 取出包裹物品
Node<E> next = f.next; // 记录下一个包裹
f.item = null; // 清空当前包裹物品
f.next = null; // 断开下一个包裹引用,帮助垃圾回收
first = next; // 新的第一个包裹
if (next == null) {
last = null; // 如果没有下一个包裹,最后一个也置空
} else {
next.prev = null; // 新第一个包裹的前一个置空
}
size--; // 包裹数量减1
return element;
}
3.2 按位置出库:删除指定位置的包裹
如果需要删除第 5 个包裹,分拣中心会先定位再操作:
java
ini
public E remove(int index) {
checkElementIndex(index); // 检查位置合法性
return unlink(node(index)); // 定位并删除
}
E unlink(Node<E> x) {
E element = x.item; // 取出物品
Node<E> next = x.next; // 下一个包裹
Node<E> prev = x.prev; // 前一个包裹
if (prev == null) {
first = next; // 如果是第一个包裹,更新头节点
} else {
prev.next = next; // 前一个包裹指向下一个包裹
x.prev = null; // 断开前一个引用
}
if (next == null) {
last = prev; // 如果是最后一个包裹,更新尾节点
} else {
next.prev = prev; // 下一个包裹指向前一个包裹
x.next = null; // 断开下一个引用
}
x.item = null; // 清空物品
size--; // 数量减1
return element;
}
四、包裹查询:LinkedList 的获取与修改
4.1 按位置找包裹
想知道第 3 个包裹是什么?分拣中心会按位置查找:
java
scss
public E get(int index) {
checkElementIndex(index); // 检查位置合法性
return node(index).item; // 定位并返回物品
}
4.2 修改包裹内容
如果发现第 2 个包裹贴错了标签,需要修改内容:
java
ini
public E set(int index, E element) {
checkElementIndex(index); // 检查位置合法性
Node<E> x = node(index); // 定位包裹
E oldVal = x.item; // 保存原物品
x.item = element; // 更新物品
return oldVal; // 返回原物品
}
五、包裹流水线:LinkedList 的迭代器
分拣中心的传送带可以从头遍历到尾,也可以从尾遍历到头,这就是迭代器的神奇之处:
java
csharp
public Iterator<E> iterator() {
return new ListItr(0); // 从头开始迭代
}
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned; // 上一个返回的包裹
private Node<E> next; // 下一个包裹
private int nextIndex; // 下一个索引
private int expectedModCount = modCount; // 期望的修改次数,用于并发检查
ListItr(int index) {
next = (index == size)? null : node(index);
nextIndex = index;
}
public E next() {
checkForComodification(); // 检查是否有并发修改
if (!hasNext()) throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
// 还有更多方法如 hasPrevious(), previous(), remove() 等
}
六、多功能传送带:LinkedList 作为队列和栈
6.1 作为队列(先进先出)
当分拣中心作为普通仓库使用时,遵循先进先出规则:
java
typescript
// 入队:尾部添加
public boolean offer(E e) {
return add(e);
}
// 出队:头部取出
public E poll() {
Node<E> f = first;
return (f == null)? null : unlinkFirst(f);
}
// 查看队首:不取出
public E peek() {
return (first == null)? null : first.item;
}
6.2 作为栈(后进先出)
当分拣中心作为紧急仓库时,遵循后进先出规则:
java
scss
// 入栈:头部添加
public void push(E e) {
addFirst(e);
}
// 出栈:头部取出
public E pop() {
return removeFirst();
}
七、分拣中心 vs 仓库货架:LinkedList vs ArrayList
场景 | LinkedList(快递分拣中心) | ArrayList(普通仓库货架) |
---|---|---|
随机取包裹 | 需要从头到尾找,慢(O (n)) | 直接按编号取,快(O (1)) |
中间插入包裹 | 只需修改指针,快(O (1)) | 需移动后面所有包裹,慢(O (n)) |
尾部添加包裹 | 直接接在末尾,快(O (1)) | 可能需要扩容,平均快(均摊 O (1)) |
空间占用 | 每个包裹有两个指针,更占空间 | 连续数组,空间紧凑 |
八、实战建议:什么时候用 LinkedList
-
推荐场景:
- 需要频繁在中间位置插入 / 删除(如聊天消息列表)
- 作为队列或栈使用(如任务调度)
- 数据量不确定,需要动态扩展
-
不推荐场景:
- 频繁随机访问(如按索引查元素)
- 数据量小且固定,插入删除不频繁
九、代码实战:LinkedList 的日常使用
java
arduino
public static void main(String[] args) {
// 创建 LinkedList
LinkedList<String> packageList = new LinkedList<>();
// 添加包裹
packageList.add("包裹1");
packageList.add("包裹2");
packageList.addFirst("包裹0"); // 头部添加
// 查看包裹
System.out.println("第一个包裹:" + packageList.getFirst());
System.out.println("第三个包裹:" + packageList.get(2));
// 修改包裹
packageList.set(1, "新包裹2");
// 删除包裹
packageList.remove(0); // 删除第一个
// 迭代遍历
System.out.println("所有包裹:");
for (String pkg : packageList) {
System.out.println(pkg);
}
// 作为栈使用
LinkedList<Integer> stack = new LinkedList<>();
stack.push(1);
stack.push(2);
System.out.println("出栈:" + stack.pop()); // 输出 2
// 作为队列使用
LinkedList<String> queue = new LinkedList<>();
queue.offer("任务1");
queue.offer("任务2");
System.out.println("出队:" + queue.poll()); // 输出 任务1
}
通过这个快递分拣中心的比喻,我们深入理解了 LinkedList
的核心原理:双向链表的结构让它在插入删除时高效如快递分拣,而迭代器和双端队列的支持让它成为多功能的数据结构工具。记住:当你的数据需要频繁 "插队" 或 "紧急出库" 时,LinkedList 就是你的最佳选择!