1. 链表基础与题目解析
链表是一种基础但极其重要的数据结构,与数组的连续存储不同,链表通过指针将零散的内存块串联起来。这道LeetCode 707题要求我们实现一个完整的链表结构,包含五种基本操作。作为面试高频题,它考察的是对链表核心操作的掌握程度。
链表通常分为单链表、双链表和循环链表三种。本题实现的是带虚拟头节点的单链表,这种设计能统一头节点和其他节点的操作逻辑,减少边界条件判断。虚拟头节点(dummy node)是一个不存储实际数据的节点,其next指针指向真正的第一个节点。
题目要求的五个函数对应着链表的五大基础操作:
- 按索引获取节点值(Get)
- 头部插入(AddAtHead)
- 尾部插入(AddAtTail)
- 指定位置插入(AddAtIndex)
- 按索引删除(DeleteAtIndex)
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 链表节点定义与初始化
链表的基础是节点结构体定义。在C++中我们这样定义:
cpp复制struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
每个节点包含:
- val:存储整型数据
- next:指向下一个节点的指针
链表管理类设计:
cpp复制class MyLinkedList {
private:
ListNode* dummyHead; // 虚拟头节点
int size; // 链表长度
public:
MyLinkedList() {
dummyHead = new ListNode(0); // 初始化虚拟头节点
size = 0;
}
// 其他成员函数...
};
初始化时创建val为0的虚拟头节点,size初始化为0。这种设计有三大优势:
- 统一插入/删除逻辑,无需特殊处理头节点
- 通过size变量可以快速判断索引有效性
- 减少空指针异常的风险
3. 获取节点操作的实现细节
获取第n个节点值的函数原型:
cpp复制int get(int index);
实现步骤:
- 检查index有效性(0 ≤ index < size)
- 从虚拟头节点开始遍历
- 移动index+1次(因为包含虚拟头节点)
- 返回当前节点的val
关键代码:
cpp复制int get(int index) {
if (index < 0 || index >= size) return -1;
ListNode* curr = dummyHead->next; // 从真实头节点开始
while (index--) curr = curr->next;
return curr->val;
}
时间复杂度:O(n),最坏情况需要遍历整个链表
空间复杂度:O(1),只使用常数空间
注意:链表索引从0开始计算,与数组一致。无效索引应返回-1。
4. 头部插入操作精讲
头部插入是最简单的插入操作,函数原型:
cpp复制void addAtHead(int val);
实现步骤:
- 创建新节点
- 新节点的next指向当前头节点
- 虚拟头节点的next指向新节点
- size增加1
关键代码:
cpp复制void addAtHead(int val) {
ListNode* newNode = new ListNode(val);
newNode->next = dummyHead->next;
dummyHead->next = newNode;
size++;
}
图示过程:
code复制初始状态:
dummy -> node1 -> node2 -> nullptr
执行addAtHead(0):
step1: 创建新节点
newNode(0)
step2: newNode->next = dummy->next
newNode(0) -> node1
step3: dummy->next = newNode
dummy -> newNode(0) -> node1 -> node2 -> nullptr
时间复杂度:O(1),固定操作次数
空间复杂度:O(1),除新节点外无额外空间
5. 尾部插入操作详解
尾部插入需要遍历到链表末尾,函数原型:
cpp复制void addAtTail(int val);
实现步骤:
- 创建新节点
- 找到最后一个节点(next为nullptr的节点)
- 最后一个节点的next指向新节点
- size增加1
关键代码:
cpp复制void addAtTail(int val) {
ListNode* curr = dummyHead;
while (curr->next) curr = curr->next;
curr->next = new ListNode(val);
size++;
}
优化技巧:
- 可以维护一个tail指针指向尾节点,将时间复杂度降为O(1)
- 但需要额外空间和更复杂的维护逻辑
时间复杂度:O(n),必须遍历整个链表
空间复杂度:O(1),除新节点外无额外空间
6. 指定位置插入操作实现
在指定索引处插入节点是最通用的插入操作,函数原型:
cpp复制void addAtIndex(int index, int val);
实现步骤:
- 检查index有效性(0 ≤ index ≤ size)
- 找到第index-1个节点(即插入位置的前驱节点)
- 创建新节点并插入
- size增加1
关键代码:
cpp复制void addAtIndex(int index, int val) {
if (index > size || index < 0) return;
ListNode* prev = dummyHead;
while (index--) prev = prev->next;
ListNode* newNode = new ListNode(val);
newNode->next = prev->next;
prev->next = newNode;
size++;
}
特殊情况处理:
- index = 0:相当于头插
- index = size:相当于尾插
- index > size:不执行操作
时间复杂度:O(n),最坏需要遍历到链表末尾
空间复杂度:O(1),除新节点外无额外空间
7. 删除节点操作完整解析
删除指定索引处的节点,函数原型:
cpp复制void deleteAtIndex(int index);
实现步骤:
- 检查index有效性(0 ≤ index < size)
- 找到第index-1个节点(即待删除节点的前驱节点)
- 修改前驱节点的next指针
- 释放被删除节点的内存
- size减1
关键代码:
cpp复制void deleteAtIndex(int index) {
if (index < 0 || index >= size) return;
ListNode* prev = dummyHead;
while (index--) prev = prev->next;
ListNode* toDelete = prev->next;
prev->next = prev->next->next;
delete toDelete;
size--;
}
内存管理要点:
- 必须手动释放删除的节点内存(C++中)
- 在Java/Python等有GC的语言中无需此步骤
- 忘记释放内存会导致内存泄漏
时间复杂度:O(n),最坏需要遍历到链表末尾
空间复杂度:O(1),只使用常数空间
8. 链表销毁与资源释放
完整的链表类需要实现析构函数释放所有节点内存:
cpp复制~MyLinkedList() {
ListNode* curr = dummyHead;
while (curr) {
ListNode* temp = curr;
curr = curr->next;
delete temp;
}
}
释放过程:
- 从虚拟头节点开始遍历
- 保存当前节点的next指针
- 释放当前节点
- 移动到下一个节点
- 直到nullptr为止
内存泄漏检查技巧:
- 使用valgrind等工具检查内存泄漏
- 在测试用例中创建销毁大量链表对象
- 确保内存使用量稳定
9. 常见问题与调试技巧
9.1 边界条件处理
链表操作最容易出错的就是边界条件:
- 空链表操作(size=0)
- 头节点操作(index=0)
- 尾节点操作(index=size-1)
- 无效索引(index<0或index≥size)
测试用例建议:
cpp复制// 边界测试
MyLinkedList list;
list.get(0); // 应该返回-1
list.deleteAtIndex(0); // 应该不报错
list.addAtIndex(1,1); // 应该不执行
// 正常流程测试
list.addAtHead(1);
list.addAtTail(3);
list.addAtIndex(1,2);
list.get(1); // 应该返回2
list.deleteAtIndex(1);
list.get(1); // 应该返回3
9.2 指针操作陷阱
链表操作中指针问题最常见:
- 空指针解引用
- 访问nullptr->next
- 解引用空指针
- 野指针问题
- 访问已释放的内存
- 指针未初始化
调试技巧:
- 在关键操作前后打印链表状态
- 使用断言检查指针有效性
- 逐步调试观察指针变化
9.3 性能优化思路
虽然题目要求的基础实现是O(n)时间复杂度,但实际应用中可以考虑:
- 维护尾指针提升尾插效率
- 实现双向链表支持反向遍历
- 使用跳表结构加速查找
- 实现内存池减少new/delete开销
10. 完整代码实现
以下是整合所有功能的完整实现:
cpp复制class MyLinkedList {
private:
struct ListNode {
int val;
ListNode *next;
ListNode(int x) : val(x), next(nullptr) {}
};
ListNode* dummyHead;
int size;
public:
MyLinkedList() : dummyHead(new ListNode(0)), size(0) {}
int get(int index) {
if (index < 0 || index >= size) return -1;
ListNode* curr = dummyHead->next;
while (index--) curr = curr->next;
return curr->val;
}
void addAtHead(int val) {
ListNode* newNode = new ListNode(val);
newNode->next = dummyHead->next;
dummyHead->next = newNode;
size++;
}
void addAtTail(int val) {
ListNode* curr = dummyHead;
while (curr->next) curr = curr->next;
curr->next = new ListNode(val);
size++;
}
void addAtIndex(int index, int val) {
if (index > size || index < 0) return;
ListNode* prev = dummyHead;
while (index--) prev = prev->next;
ListNode* newNode = new ListNode(val);
newNode->next = prev->next;
prev->next = newNode;
size++;
}
void deleteAtIndex(int index) {
if (index < 0 || index >= size) return;
ListNode* prev = dummyHead;
while (index--) prev = prev->next;
ListNode* toDelete = prev->next;
prev->next = prev->next->next;
delete toDelete;
size--;
}
~MyLinkedList() {
ListNode* curr = dummyHead;
while (curr) {
ListNode* temp = curr;
curr = curr->next;
delete temp;
}
}
};
11. 链表操作复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 获取元素 | O(n) | O(1) |
| 头部插入 | O(1) | O(1) |
| 尾部插入 | O(n) | O(1) |
| 指定位置插入 | O(n) | O(1) |
| 删除元素 | O(n) | O(1) |
从表格可以看出,链表在头部操作高效,但随机访问和尾部操作效率较低。这正是链表与数组的核心区别:数组支持O(1)随机访问但插入删除效率低,链表则相反。
12. 链表应用场景与变种
虽然基础链表操作看起来简单,但链表在实际工程中有广泛应用:
-
操作系统内核:
- 进程调度队列
- 内存管理空闲块链表
- 文件描述符管理
-
数据库系统:
- 哈希表冲突解决
- B+树的叶子节点链接
- 事务日志记录
-
高级数据结构基础:
- 栈和队列的实现
- 图的邻接表表示
- 跳表的基础结构
常见链表变种:
- 双向链表:每个节点增加prev指针
- 循环链表:尾节点指向头节点
- 静态链表:用数组实现的链表
- 异或链表:用异或运算压缩指针
13. 链表题目进阶训练
掌握基础链表操作后,可以挑战以下LeetCode进阶题目:
-
反转链表(#206)
- 迭代法和递归法两种解法
- 需要熟练操作指针指向
-
环形链表检测(#141)
- 快慢指针技巧
- 判断链表是否有环
-
合并两个有序链表(#21)
- 递归和迭代两种解法
- 练习指针操作和边界处理
-
删除倒数第N个节点(#19)
- 快慢指针经典应用
- 一次遍历解法
-
相交链表(#160)
- 双指针技巧
- 空间复杂度O(1)的解法
链表题目解题技巧总结:
- 虚拟头节点统一操作
- 快慢指针解决环和中间节点问题
- 递归法简化某些操作
- 画图辅助理解指针变化
