深入浅出:C++ 链表完全指南
从零开始掌握链表 | 图文并茂 | 代码实战
一、什么是链表?
链表(Linked List)是一种线性数据结构,它由一系列节点(Node)组成。与数组不同的是,链表中的元素在内存中不是连续存储的,而是通过指针连接在一起。
🚂 火车车厢的比喻
想象一列火车🚂,每节车厢就是一个节点:
- 车厢本身:装载货物(数据)
- 车厢挂钩:连接下一节车厢(指针)
- 车头:第一节车厢(头指针 HEAD)
你可以随时加挂或卸下车厢(插入/删除),但要找到第5节车厢,必须从车头开始一节一节数过去。这就是链表的工作原理!
链表节点的结构
每个链表节点包含两个关键部分:
┌─────────────┬─────────────┐
│ 数据域 │ 指针域 │
│ Data │ Next → │
└─────────────┴─────────────┘
完整链表示例:
HEAD↓
┌────┬──┐ ┌────┬──┐ ┌────┬──┐
│ 10 │→ │ → │ 20 │→ │ → │ 30 │∅ │
└────┴──┘ └────┴──┘ └────┴──┘节点1 节点2 节点3(尾)
C++ 代码实现:
// C++ 链表节点定义
struct Node {int data; // 数据域:存储实际数据Node* next; // 指针域:指向下一个节点// 构造函数Node(int val) : data(val), next(nullptr) {}
};
二、链表 vs 数组:该如何选择?
理解链表的最佳方式就是将它与数组进行对比:
特性 | 链表 🔗 | 数组 📦 |
---|---|---|
内存分配 | 分散的,动态分配 | 连续的,静态或动态 |
大小 | 可动态增长 | 固定大小(静态数组) |
访问元素 | O(n) - 需要遍历 | O(1) - 直接索引 |
插入/删除 | O(1) - 只需改变指针 | O(n) - 需要移动元素 |
内存开销 | 额外的指针空间 | 无额外开销 |
缓存友好性 | 较差(内存不连续) | 很好(连续内存) |
💡 何时使用链表?
- ✅ 需要频繁插入和删除操作
- ✅ 数据量不确定,需要动态增长
- ✅ 不需要随机访问元素
- ✅ 实现栈、队列等其他数据结构
💡 何时使用数组?
- ✅ 需要快速随机访问
- ✅ 数据量固定或变化不大
- ✅ 内存连续性很重要(缓存优化)
- ✅ 很少进行插入删除操作
三、链表的基本操作
1. 在头部插入节点
这是最简单的操作,时间复杂度为 O(1):
void insertAtHead(int val) {Node* newNode = new Node(val); // 创建新节点newNode->next = head; // 新节点指向原头节点head = newNode; // 更新头指针
}
图解:
插入前: HEAD → [10] → [20] → NULL插入5: [5] → [10] → [20] → NULL↑HEAD
2. 在尾部插入节点
需要遍历到最后一个节点,时间复杂度为 O(n):
void insertAtTail(int val) {Node* newNode = new Node(val);if (head == nullptr) { // 如果链表为空head = newNode;return;}Node* temp = head;while (temp->next != nullptr) { // 遍历到最后一个节点temp = temp->next;}temp->next = newNode; // 连接新节点
}
图解:
插入前: HEAD → [10] → [20] → NULL遍历: HEAD → [10] → [20] → NULL↑temp插入30: HEAD → [10] → [20] → [30] → NULL
3. 删除节点
删除节点需要找到前一个节点,修改其指针:
void deleteByValue(int val) {if (head == nullptr) return;// 如果要删除的是头节点if (head->data == val) {Node* temp = head;head = head->next;delete temp;return;}// 查找要删除的节点Node* temp = head;while (temp->next != nullptr && temp->next->data != val) {temp = temp->next;}if (temp->next != nullptr) {Node* nodeToDelete = temp->next;temp->next = temp->next->next; // 跳过要删除的节点delete nodeToDelete; // 释放内存}
}
图解:
删除前: HEAD → [10] → [20] → [30] → NULL删除20: HEAD → [10] ───────┐↓[20]× [30] → NULL(删除)删除后: HEAD → [10] → [30] → NULL
4. 搜索节点
遍历链表查找特定值:
bool search(int val) {Node* temp = head;while (temp != nullptr) {if (temp->data == val) {return true;}temp = temp->next;}return false;
}
5. 反转链表 ⭐
这是一个经典的面试题,需要改变所有指针的方向:
void reverse() {Node* prev = nullptr;Node* current = head;Node* next = nullptr;while (current != nullptr) {next = current->next; // 保存下一个节点current->next = prev; // 反转指针prev = current; // 移动prevcurrent = next; // 移动current}head = prev; // 更新头指针
}
图解:
原链表: HEAD → [10] → [20] → [30] → NULL步骤1: NULL ← [10] [20] → [30] → NULL↑ ↑prev current步骤2: NULL ← [10] ← [20] [30] → NULL↑ ↑prev current步骤3: NULL ← [10] ← [20] ← [30]↑HEAD反转后: HEAD → [30] → [20] → [10] → NULL
6. 获取链表长度
int length() {int count = 0;Node* temp = head;while (temp != nullptr) {count++;temp = temp->next;}return count;
}
7. 打印链表
void display() {if (head == nullptr) {cout << "链表为空" << endl;return;}Node* temp = head;while (temp != nullptr) {cout << temp->data;if (temp->next != nullptr) {cout << " -> ";}temp = temp->next;}cout << endl;
}
四、完整的链表类实现
#include <iostream>
using namespace std;// 定义链表节点结构
struct Node {int data;Node* next;Node(int val) : data(val), next(nullptr) {}
};// 链表类
class LinkedList {
private:Node* head;public:// 构造函数LinkedList() : head(nullptr) {}// 析构函数 - 释放所有节点~LinkedList() {Node* current = head;while (current != nullptr) {Node* temp = current;current = current->next;delete temp;}}// 在头部插入void insertAtHead(int val) {Node* newNode = new Node(val);newNode->next = head;head = newNode;}// 在尾部插入void insertAtTail(int val) {Node* newNode = new Node(val);if (head == nullptr) {head = newNode;return;}Node* temp = head;while (temp->next != nullptr) {temp = temp->next;}temp->next = newNode;}// 删除指定值的节点void deleteByValue(int val) {if (head == nullptr) return;if (head->data == val) {Node* temp = head;head = head->next;delete temp;return;}Node* temp = head;while (temp->next != nullptr && temp->next->data != val) {temp = temp->next;}if (temp->next != nullptr) {Node* nodeToDelete = temp->next;temp->next = temp->next->next;delete nodeToDelete;}}// 搜索节点bool search(int val) {Node* temp = head;while (temp != nullptr) {if (temp->data == val) return true;temp = temp->next;}return false;}// 反转链表void reverse() {Node* prev = nullptr;Node* current = head;Node* next = nullptr;while (current != nullptr) {next = current->next;current->next = prev;prev = current;current = next;}head = prev;}// 获取长度int length() {int count = 0;Node* temp = head;while (temp != nullptr) {count++;temp = temp->next;}return count;}// 打印链表void display() {if (head == nullptr) {cout << "链表为空" << endl;return;}Node* temp = head;while (temp != nullptr) {cout << temp->data;if (temp->next != nullptr) cout << " -> ";temp = temp->next;}cout << endl;}
};// 测试代码
int main() {LinkedList list;cout << "=== 链表操作演示 ===" << endl;// 插入操作list.insertAtTail(10);list.insertAtTail(20);list.insertAtTail(30);cout << "插入 10, 20, 30: ";list.display();list.insertAtHead(5);cout << "头部插入 5: ";list.display();// 搜索操作cout << "搜索 20: " << (list.search(20) ? "找到" : "未找到") << endl;// 删除操作list.deleteByValue(20);cout << "删除 20: ";list.display();// 反转链表list.reverse();cout << "反转链表: ";list.display();// 获取长度cout << "链表长度: " << list.length() << endl;return 0;
}
输出:
=== 链表操作演示 ===
插入 10, 20, 30: 10 -> 20 -> 30
头部插入 5: 5 -> 10 -> 20 -> 30
搜索 20: 找到
删除 20: 5 -> 10 -> 30
反转链表: 30 -> 10 -> 5
链表长度: 3
五、链表的类型
1. 单链表(Single Linked List)
每个节点只有一个指向下一个节点的指针,这是我们上面讨论的类型。
HEAD → [A] → [B] → [C] → NULL
2. 双链表(Double Linked List)
每个节点有两个指针,分别指向前一个和后一个节点,可以双向遍历。
struct DoubleNode {int data;DoubleNode* prev; // 指向前一个节点DoubleNode* next; // 指向后一个节点
};
NULL ← [A] ⇄ [B] ⇄ [C] → NULL↑HEAD
3. 循环链表(Circular Linked List)
最后一个节点的指针指向第一个节点,形成一个环。
┌─────────────────┐↓ │
HEAD → [A] → [B] → [C] ─┘
📊 应用场景对比
链表类型 | 主要应用场景 |
---|---|
单链表 | 简单的顺序访问、栈实现 |
双链表 | 浏览器前进后退、LRU缓存 |
循环链表 | 音乐播放器循环播放、操作系统进程调度 |
六、链表的常见面试题
1. 检测链表中的环(Floyd判圈算法)
使用快慢指针,如果链表有环,快慢指针最终会相遇。
bool hasCycle(Node* head) {if (!head) return false;Node* slow = head;Node* fast = head;while (fast && fast->next) {slow = slow->next; // 慢指针走一步fast = fast->next->next; // 快指针走两步if (slow == fast) {return true; // 相遇,说明有环}}return false;
}
原理图解:
有环的链表:[1] → [2] → [3] → [4]↑ ↓←-- [6] ← [5]slow: 1 → 2 → 3 → 4 → 5 → 6 → 3 → 4...
fast: 1 → 3 → 5 → 3...在节点3相遇!
2. 找到链表的中间节点
同样使用快慢指针:
Node* findMiddle(Node* head) {Node* slow = head;Node* fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}return slow; // 快指针到末尾时,慢指针在中间
}
示例:
链表: [1] → [2] → [3] → [4] → [5] → NULLslow: 1 2 3 4 5
fast: 1 3 5 NULL↑中间节点
3. 合并两个有序链表
Node* mergeLists(Node* l1, Node* l2) {Node dummy(0); // 哨兵节点Node* tail = &dummy;while (l1 && l2) {if (l1->data < l2->data) {tail->next = l1;l1 = l1->next;} else {tail->next = l2;l2 = l2->next;}tail = tail->next;}tail->next = l1 ? l1 : l2; // 连接剩余部分return dummy.next;
}
4. 删除链表的倒数第N个节点
使用双指针,先让快指针走N步,然后快慢指针同时移动:
Node* removeNthFromEnd(Node* head, int n) {Node dummy(0);dummy.next = head;Node* fast = &dummy;Node* slow = &dummy;// 快指针先走n+1步for (int i = 0; i <= n; i++) {fast = fast->next;}// 快慢指针同时移动while (fast != nullptr) {fast = fast->next;slow = slow->next;}// 删除节点Node* temp = slow->next;slow->next = slow->next->next;delete temp;return dummy.next;
}
5. 判断链表是否为回文
bool isPalindrome(Node* head) {// 找到中间节点Node* slow = head;Node* fast = head;while (fast && fast->next) {slow = slow->next;fast = fast->next->next;}// 反转后半部分Node* prev = nullptr;while (slow) {Node* next = slow->next;slow->next = prev;prev = slow;slow = next;}// 比较前半部分和反转后的后半部分Node* left = head;Node* right = prev;while (right) {if (left->data != right->data) {return false;}left = left->next;right = right->next;}return true;
}
七、时间复杂度总结
操作 | 时间复杂度 | 说明 |
---|---|---|
访问 | O(n) | 需要从头遍历 |
搜索 | O(n) | 需要遍历查找 |
头部插入 | O(1) | 直接修改头指针 |
尾部插入 | O(n) | 需要遍历到末尾 |
头部删除 | O(1) | 直接修改头指针 |
尾部删除 | O(n) | 需要找到倒数第二个节点 |
中间插入/删除 | O(n) | 需要先找到位置 |
优化方案
如果需要频繁在尾部插入,可以维护一个 tail
指针:
class LinkedList {
private:Node* head;Node* tail; // 维护尾指针public:void insertAtTail(int val) {Node* newNode = new Node(val);if (!head) {head = tail = newNode;} else {tail->next = newNode;tail = newNode;}} // 现在是 O(1)!
};
八、实际应用场景
1. 浏览器历史记录
使用双链表实现前进和后退功能:
struct HistoryNode {string url;HistoryNode* prev;HistoryNode* next;
};class BrowserHistory {
private:HistoryNode* current;public:void visit(string url) {HistoryNode* newPage = new HistoryNode{url, current, nullptr};if (current) current->next = newPage;current = newPage;}void back() {if (current && current->prev) {current = current->prev;}}void forward() {if (current && current->next) {current = current->next;}}
};
2. LRU 缓存
使用双链表 + 哈希表实现:
class LRUCache {
private:int capacity;list<pair<int, int>> cache; // 双链表unordered_map<int, list<pair<int, int>>::iterator> map;public:LRUCache(int cap) : capacity(cap) {}int get(int key) {if (map.find(key) == map.end()) return -1;// 移到链表头部auto it = map[key];int value = it->second;cache.erase(it);cache.push_front({key, value});map[key] = cache.begin();return value;}void put(int key, int value) {if (map.find(key) != map.end()) {cache.erase(map[key]);} else if (cache.size() >= capacity) {// 删除最久未使用的int oldKey = cache.back().first;cache.pop_back();map.erase(oldKey);}cache.push_front({key, value});map[key] = cache.begin();}
};
3. 音乐播放器(循环链表)
struct Song {string title;Song* next;
};class MusicPlayer {
private:Song* current;public:void nextSong() {if (current) current = current->next;// 如果是循环链表,永远不会为空}void play() {if (current) {cout << "正在播放: " << current->title << endl;}}
};
九、学习建议与总结
💡 学习链表的最佳实践
-
画图!画图!画图!
- 在纸上画出节点和指针的变化过程
- 这是理解指针操作的最佳方法
-
理解指针
- 链表的核心是指针操作
- 确保你理解
*
和->
的含义
-
注意边界情况
- 空链表(head == nullptr)
- 单节点链表
- 删除头节点或尾节点
-
内存管理
- 不要忘记
delete
删除的节点 - 使用析构函数释放所有节点
- 不要忘记
-
多练习
- 在 LeetCode 上刷链表题
- 推荐题目:206, 141, 21, 160, 234
🎯 常见陷阱
// ❌ 错误:直接修改会丢失链表
Node* temp = head;
head = head->next; // 如果没有保存temp,原head会泄漏// ✅ 正确:先保存,再删除
Node* temp = head;
head = head->next;
delete temp;// ❌ 错误:空指针访问
if (head->next->data == 5) // 如果head或head->next为空会崩溃// ✅ 正确:检查空指针
if (head && head->next && head->next->data == 5)
📚 进阶学习路径
-
基础链表 ✅
- 单链表的实现和操作
-
进阶链表
- 双链表
- 循环链表
- 跳表(Skip List)
-
应用实践
- 实现栈和队列
- LRU 缓存实现
- 图的邻接表表示
-
算法竞赛
- 链表相关的经典算法题
- 复杂的指针操作技巧
🔍 与标准库的比较
C++ 标准库提供了 std::list
(双链表)和 std::forward_list
(单链表):
#include <list>
#include <forward_list>// 双链表
std::list<int> myList = {1, 2, 3};
myList.push_back(4);
myList.push_front(0);// 单链表
std::forward_list<int> fList = {1, 2, 3};
fList.push_front(0);
何时使用自己实现的链表?
- 学习和面试准备
- 需要定制化的功能
- 嵌入式系统(避免STL开销)
- 理解底层原理
总结
链表是计算机科学中最基础也是最重要的数据结构之一。虽然在现代编程中,我们更多地使用标准库提供的容器,但理解链表的原理对于:
✅ 理解其他复杂数据结构(如图、树)
✅ 掌握指针操作和内存管理
✅ 通过技术面试
✅ 优化程序性能
都有着重要的意义。
记住这个学习口诀:
画图理解,代码实践,多做题目,注意边界!
参考资源
- 📖 《数据结构与算法分析》- Mark Allen Weiss
- 📖 《算法导论》- CLRS
- 💻 LeetCode 链表题目集
- 💻 cppreference.com - C++ 标准库文档