链表详解
1. 什么是链表
链表(Linked List)是一种线性数据结构,由一系列节点(Node)组成,每个节点包含数据域和指针域。与数组不同,链表中的元素在内存中不是连续存储的,而是通过指针链接在一起。
链表主要分为:
- 单链表(Singly Linked List):每个节点只有一个指针指向下一个节点
┌───┐ ┌───┐ ┌───┐
│ A │ ──>│ B │ ──>│ C │ ──> null
└───┘ └───┘ └───┘
- 双向链表(Doubly Linked List):每个节点有两个指针,分别指向前驱和后继节点
null <── ┌───┐ <──> ┌───┐ <──> ┌───┐ <──> null
│ A │ │ B │ │ C │
└───┘ └───┘ └───┘
- 循环链表(Circular Linked List):尾节点指向头节点形成环状结构
┌───┐ ┌───┐ ┌───┐
│ A │ ──>│ B │ ──>│ C │
└───┘ └───┘ └───┘
▲ │
└──────────────────┘
2. 链表的特性
- 动态大小:链表可以根据需要动态增长或缩小,不需要预先分配固定大小的内存空间
- 高效插入/删除:在已知节点位置的情况下,插入和删除操作的时间复杂度为O(1)
- 非连续存储:节点在内存中分散存储,通过指针连接
- 顺序访问:只能从头节点开始顺序访问,无法直接访问任意位置的节点
3. 链表的优缺点
3.1 优点
- 动态内存分配,不需要预先知道数据规模
- 插入和删除操作高效,只需修改指针指向
- 可以高效实现栈、队列等抽象数据类型
- 不会造成内存空间的浪费
3.2 缺点
- 访问元素需要从头遍历,随机访问效率低(O(n))
- 需要额外的内存空间存储指针
- 缓存不友好,因为节点在内存中不连续
- 反向遍历困难(单链表)
4. 链表与数组性能对比
| 操作 | 链表时间复杂度 | 数组时间复杂度 |
|---|---|---|
| 访问元素 | O(n) | O(1) |
| 插入/删除 | O(1) | O(n) |
| 查找元素 | O(n) | O(n) |
| 内存占用 | 较高(指针开销) | 较低 |
5. 链表的基本使用示例
// 单链表基本操作示例
// 1. 创建链表实例
SinglyLinkedList<String> list = new SinglyLinkedList<>();
// 2. 添加元素 - 时间复杂度O(1)
list.add("Java"); // 头节点指向新节点
list.add("Python"); // 新节点插入链表末尾
list.add("C++"); // 再次插入末尾
// 3. 遍历链表 - 时间复杂度O(n)
Node<String> current = list.getHead(); // 获取头节点
while (current != null) {
System.out.println(current.data); // 打印当前节点数据
current = current.next; // 移动到下一个节点
}
6. 单链表核心方法实现
6.1 插入操作
// 在指定位置插入元素
public void insert(int index, T data) {
if (index < 0 || index > size) {
throw new IndexOutOfBoundsException();
}
Node<T> newNode = new Node<>(data);
if (index == 0) {
// 插入到头部
newNode.next = head;
head = newNode;
} else {
// 找到插入位置的前驱节点
Node<T> prev = head;
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
newNode.next = prev.next;
prev.next = newNode;
}
size++;
}
6.2 删除操作
// 删除指定位置的元素
public T remove(int index) {
if (index < 0 || index >= size) {
throw new IndexOutOfBoundsException();
}
T removedData;
if (index == 0) {
// 删除头节点
removedData = head.data;
head = head.next;
} else {
// 找到要删除节点的前驱节点
Node<T> prev = head;
for (int i = 0; i < index - 1; i++) {
prev = prev.next;
}
removedData = prev.next.data;
prev.next = prev.next.next;
}
size--;
return removedData;
}
6.3 查找操作
// 查找元素是否存在
public boolean contains(T data) {
Node<T> current = head;
while (current != null) {
if (current.data.equals(data)) {
return true;
}
current = current.next;
}
return false;
}
常见错误分析:
- 忘记检查current是否为null导致NullPointerException
- 在遍历过程中修改链表结构可能导致ConcurrentModificationException
- 循环链表未正确处理终止条件会导致无限循环
7. 内存占用分析
- 单链表节点内存:数据域(4-8字节) + 指针域(4-8字节)
- 双向链表节点内存:数据域(4-8字节) + 前驱指针(4-8字节) + 后继指针(4-8字节)
- 32位系统指针4字节,64位系统指针8字节
8. 并发环境下的线程安全
- 使用锁机制保护链表操作
- 使用并发集合如ConcurrentLinkedQueue
- 采用不可变链表设计
- 读写锁优化读多写少场景
9. 实际项目应用案例
- Java LinkedList实现
- Redis的列表数据结构
- Linux内核任务调度
- 浏览器历史记录管理
- 游戏中的对象池管理
10. 完整代码
本文中的代码示例可以在我的GitHub仓库中找到:查看完整代码