"数组是连号的酒店房间,链表是手拉手的小朋友!" 🤝
😊 什么是链表?火车的比喻
还记得小时候玩的玩具火车吗?🚂
每节车厢都有一个挂钩,连接着下一节车厢。想要加一节车厢?很简单,挂上去就行!想要去掉一节?解开挂钩就好!
🚂 单向火车(单向链表):
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
│ 车头 │→│ 车厢1│→│ 车厢2│→│ 车尾 │→ null
│[10] │ │[20] │ │[30] │ │[40] │
└─────┘ └─────┘ └─────┘ └─────┘
这就是链表! 每个"车厢"(节点)里装着数据,还有一个"挂钩"(指针)指向下一节车厢。
🎯 链表的三兄弟
1️⃣ 单向链表(Singly Linked List)
特点:只能从头往后走,不能倒退 ➡️
节点结构:
┌─────────┬─────────┐
│ data │ next │ ← 每个节点有两部分
│ 数据域 │ 指针域 │
└─────────┴─────────┘
完整链表:
head
↓
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ 1 │→ │ 2 │→ │ 3 │→ │ 4 │→ null
└───┘ └───┘ └───┘ └───┘
生活例子:
- 🎬 看电影的时候,你知道"下一集"是什么,但不知道"上一集"是哪个
- 📖 翻书只能往后翻,书页上写着"下一页在这里"
2️⃣ 双向链表(Doubly Linked List)
特点:既能往后走,也能往前退 ↔️
节点结构:
┌─────┬──────┬──────┐
│prev │ data │ next │ ← 有三部分
│前驱 │ 数据 │ 后继 │
└─────┴──────┴──────┘
完整链表:
head
↓
┌───┐ ⇄ ┌───┐ ⇄ ┌───┐ ⇄ ┌───┐
│ 1 │ │ 2 │ │ 3 │ │ 4 │
└───┘ └───┘ └───┘ └───┘
↑ ↓
null null
生活例子:
- 🚇 地铁,你既知道上一站,也知道下一站
- 📱 浏览器的"前进"和"后退"按钮
3️⃣ 循环链表(Circular Linked List)
特点:首尾相连,形成一个环 🔄
单向循环:
head
↓
┌───┐ ┌───┐ ┌───┐ ┌───┐
│ 1 │→ │ 2 │→ │ 3 │→ │ 4 │→┐
└───┘ └───┘ └───┘ └───┘ │
↑ │
└────────────────────────────┘
双向循环:
head
↓
┌───┐ ⇄ ┌───┐ ⇄ ┌───┐ ⇄ ┌───┐
│ 1 │ │ 2 │ │ 3 │ │ 4 │
└───┘ └───┘ └───┘ └───┘
↑ ↓
└──────────────────────────┘
生活例子:
- 🎡 摩天轮,转啊转,永远在循环
- 🔁 音乐播放器的"循环播放"模式
🆚 数组 vs 链表:终极PK
| 对比项 | 数组🥚 | 链表🚂 |
|---|---|---|
| 内存 | 连续的格子 | 分散的车厢 |
| 访问速度 | ⚡超快 O(1) | 🐌较慢 O(n) |
| 插入删除 | 🐌慢 O(n) | ⚡快 O(1) |
| 大小 | 😰 固定 | 😊 灵活 |
| 空间利用 | 可能浪费 | 按需分配 |
| 额外空间 | 无 | 需要指针 |
形象比喻:
- 数组:一排连号的酒店房间(1001, 1002, 1003...),找房间快,但中间换房客很麻烦
- 链表:手拉手的小朋友,中间插入或离开都很方便,但要找第n个小朋友得从头数
💻 Java代码实现
单向链表实现
// 定义节点
class Node {
int data; // 数据域:存储数据
Node next; // 指针域:指向下一个节点
Node(int data) {
this.data = data;
this.next = null;
}
}
// 单向链表
public class SinglyLinkedList {
Node head; // 头节点
// 1. 在头部插入 - O(1)
public void insertAtHead(int data) {
Node newNode = new Node(data);
newNode.next = head;
head = newNode;
System.out.println("✅ 在头部插入: " + data);
}
// 2. 在尾部插入 - O(n)
public void insertAtTail(int data) {
Node newNode = new Node(data);
if (head == null) {
head = newNode;
return;
}
Node current = head;
while (current.next != null) {
current = current.next;
}
current.next = newNode;
System.out.println("✅ 在尾部插入: " + data);
}
// 3. 删除指定值的节点 - O(n)
public void delete(int data) {
if (head == null) return;
// 删除头节点
if (head.data == data) {
head = head.next;
System.out.println("🗑️ 删除节点: " + data);
return;
}
// 删除其他节点
Node current = head;
while (current.next != null && current.next.data != data) {
current = current.next;
}
if (current.next != null) {
current.next = current.next.next;
System.out.println("🗑️ 删除节点: " + data);
}
}
// 4. 查找节点 - O(n)
public boolean search(int data) {
Node current = head;
while (current != null) {
if (current.data == data) {
return true;
}
current = current.next;
}
return false;
}
// 5. 打印链表
public void printList() {
if (head == null) {
System.out.println("链表为空 🈳");
return;
}
Node current = head;
System.out.print("链表: ");
while (current != null) {
System.out.print(current.data);
if (current.next != null) {
System.out.print(" → ");
}
current = current.next;
}
System.out.println(" → null");
}
}
// 测试代码
public class Main {
public static void main(String[] args) {
SinglyLinkedList list = new SinglyLinkedList();
list.insertAtHead(10); // 10 → null
list.insertAtHead(20); // 20 → 10 → null
list.insertAtTail(30); // 20 → 10 → 30 → null
list.insertAtTail(40); // 20 → 10 → 30 → 40 → null
list.printList();
list.delete(10); // 20 → 30 → 40 → null
list.printList();
System.out.println("查找30: " + list.search(30)); // true
System.out.println("查找99: " + list.search(99)); // false
}
}
双向链表实现
// 双向链表节点
class DoublyNode {
int data;
DoublyNode prev; // 前驱指针
DoublyNode next; // 后继指针
DoublyNode(int data) {
this.data = data;
this.prev = null;
this.next = null;
}
}
// 双向链表
public class DoublyLinkedList {
DoublyNode head;
DoublyNode tail;
// 在头部插入
public void insertAtHead(int data) {
DoublyNode newNode = new DoublyNode(data);
if (head == null) {
head = tail = newNode;
} else {
newNode.next = head;
head.prev = newNode;
head = newNode;
}
}
// 在尾部插入
public void insertAtTail(int data) {
DoublyNode newNode = new DoublyNode(data);
if (tail == null) {
head = tail = newNode;
} else {
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
}
}
// 从前往后打印
public void printForward() {
DoublyNode current = head;
System.out.print("正向: null ⇄ ");
while (current != null) {
System.out.print(current.data);
if (current.next != null) {
System.out.print(" ⇄ ");
}
current = current.next;
}
System.out.println(" ⇄ null");
}
// 从后往前打印
public void printBackward() {
DoublyNode current = tail;
System.out.print("反向: null ⇄ ");
while (current != null) {
System.out.print(current.data);
if (current.prev != null) {
System.out.print(" ⇄ ");
}
current = current.prev;
}
System.out.println(" ⇄ null");
}
}
🎬 链表操作动画演示
插入操作
在中间插入新节点:
步骤1:原链表
head
↓
[1] → [2] → [4] → null
步骤2:创建新节点 [3]
newNode = [3]
步骤3:调整指针(在2和4之间插入3)
[2].next = [3]
[3].next = [4]
步骤4:完成!
head
↓
[1] → [2] → [3] → [4] → null
删除操作
删除中间节点:
步骤1:原链表
head
↓
[1] → [2] → [3] → [4] → null
步骤2:找到要删除节点[3]的前一个节点[2]
↓
[1] → [2] → [3] → [4] → null
步骤3:跳过节点[3]
[2].next = [4]
步骤4:完成!([3]被回收)
head
↓
[1] → [2] → [4] → null
🏆 链表的经典面试题
1. 反转链表(LeetCode 206)⭐⭐⭐
输入: 1 → 2 → 3 → 4 → 5 → null
输出: 5 → 4 → 3 → 2 → 1 → null
public Node reverseList(Node head) {
Node prev = null;
Node current = head;
while (current != null) {
Node nextTemp = current.next; // 保存下一个节点
current.next = prev; // 反转指针
prev = current; // 往前移动
current = nextTemp;
}
return prev; // 新的头节点
}
图解:
原始: null ← [1] → [2] → [3] → null
prev curr next
步骤1: null ← [1] [2] → [3] → null
prev curr
步骤2: null ← [1] ← [2] [3] → null
prev curr
步骤3: null ← [1] ← [2] ← [3]
prev
2. 环形链表检测(快慢指针)⭐⭐⭐
问题:判断链表是否有环 🔄
public boolean hasCycle(Node head) {
if (head == null) return false;
Node slow = head; // 慢指针:每次走1步 🐢
Node fast = head; // 快指针:每次走2步 🐰
while (fast != null && fast.next != null) {
slow = slow.next; // 慢指针走1步
fast = fast.next.next; // 快指针走2步
if (slow == fast) { // 相遇了!有环!
return true;
}
}
return false; // 快指针到终点了,无环
}
生活比喻: 两个人在操场跑圈🏃,一个跑得快,一个跑得慢。如果操场是环形的,快的人迟早会追上慢的人(套圈)。如果是直线跑道,快的人就跑到终点了,不会相遇。
3. 链表的中间节点
public Node findMiddle(Node head) {
Node slow = head;
Node fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow; // 慢指针刚好在中间
}
4. 合并两个有序链表
public Node mergeTwoLists(Node l1, Node l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.data < l2.data) {
l1.next = mergeTwoLists(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
📊 链表的时间复杂度总结
| 操作 | 单向链表 | 双向链表 | 数组(对比) |
|---|---|---|---|
| 访问第i个元素 | O(n) 🐌 | O(n) 🐌 | O(1) ⚡ |
| 头部插入 | O(1) ⚡ | O(1) ⚡ | O(n) 🐌 |
| 尾部插入 | O(n) 🐌 | O(1) ⚡(有tail) | O(1) ⚡ |
| 头部删除 | O(1) ⚡ | O(1) ⚡ | O(n) 🐌 |
| 中间插入 | O(n) | O(n) | O(n) |
| 查找元素 | O(n) | O(n) | O(n) |
🎯 什么时候用链表?
✅ 适合用链表的场景:
-
频繁插入删除 - 比如音乐播放列表 🎵
// 添加歌曲、删除歌曲很频繁 LinkedList<Song> playlist = new LinkedList<>(); -
不知道数据量 - 数据量动态变化 📈
-
不需要随机访问 - 只需要顺序遍历 ➡️
-
实现栈和队列 - LinkedList可以当栈和队列用 📚
❌ 不适合用链表的场景:
- 需要快速随机访问 - 用数组 🎯
- 内存紧张 - 链表的指针占用额外空间 💾
- 数据量小且固定 - 数组更简单 📦
🌟 Java的LinkedList类
Java提供了现成的LinkedList类(双向链表实现):
import java.util.LinkedList;
LinkedList<Integer> list = new LinkedList<>();
// 添加元素
list.add(10); // 尾部添加
list.addFirst(5); // 头部添加
list.addLast(20); // 尾部添加
// 删除元素
list.removeFirst(); // 删除头部
list.removeLast(); // 删除尾部
list.remove(1); // 删除索引为1的元素
// 获取元素
int first = list.getFirst(); // 获取第一个
int last = list.getLast(); // 获取最后一个
int element = list.get(2); // 获取索引为2的元素
// 遍历
for (int num : list) {
System.out.println(num);
}
LinkedList vs ArrayList:
LinkedList: 🚂🚂🚂 (插删快,访问慢)
ArrayList: 📦📦📦 (访问快,插删慢)
💡 实战应用场景
1. 浏览器的前进后退
用双向链表实现:
class BrowserHistory {
DoublyNode current;
// 访问新页面
void visit(String url) { ... }
// 后退
String back() { return current.prev.data; }
// 前进
String forward() { return current.next.data; }
}
2. 音乐播放器
用循环链表实现循环播放:
class MusicPlayer {
CircularNode currentSong;
// 下一首
void next() { currentSong = currentSong.next; }
// 上一首
void previous() { currentSong = currentSong.prev; }
}
3. LRU缓存
用双向链表 + HashMap实现:
// 最近使用的在头部,最久未用的在尾部
// 达到容量上限时,删除尾部节点
📝 总结
🎓 记忆口诀
链表像火车,节节相连,
单向往前走,双向能倒退,
循环首尾接,永远在转圈。
插入删除快,访问有点慢,
空间很灵活,指针占空间。
头尾操作易,中间需遍历!
📊 核心特点
| 特性 | 说明 | 表情 |
|---|---|---|
| 内存 | 不连续,灵活分配 | 🧩 |
| 插删 | 头尾快O(1),中间O(n) | ⚡ |
| 访问 | 慢O(n),需遍历 | 🐌 |
| 空间 | 需要额外指针 | 📍 |
🚀 下一步学习
掌握了链表,接下来可以学习:
- 跳表(Skip List) - 链表的升级版,Redis用它! 🎯
- 栈(Stack) - 可以用链表实现 📚
- 队列(Queue) - 也可以用链表实现 🎫
恭喜你!🎉 你已经掌握了链表这个灵活的数据结构!
记住:数组是连号房间,链表是手拉手的小朋友! 各有千秋,选对场景最重要!💪
📌 小练习:尝试手写一个单向链表,实现插入、删除、反转三个操作!
🤔 思考题:为什么Java的LinkedList要用双向链表而不是单向链表?
(答案:双向链表可以从两端操作,实现Deque接口更高效!)