题目
设计并实现一个链表,支持以下操作:
get(index):获取下标为index的节点值,下标无效时返回-1;addAtHead(val):在链表头部插入节点;addAtTail(val):在链表尾部插入节点;addAtIndex(index, val):在下标index对应节点之前插入;当index == size时追加到尾部,index > size时不插入;deleteAtIndex(index):下标有效时删除对应节点。
本题不能直接使用语言内置的链表容器。
示例:
❯Text
操作:
MyLinkedList()
addAtHead(1)
addAtTail(3)
addAtIndex(1, 2)
get(1)
deleteAtIndex(1)
get(1)
链表变化:
[] → [1] → [1,3] → [1,2,3] → [1,3]
返回结果:
get(1) 第一次返回 2,第二次返回 3提示:
0 <= index, val <= 1000- 所有接口的调用次数不超过
2000
考察内容
- 单链表节点结构
- 虚拟头节点与前驱节点
- 按下标遍历、插入和删除
- 链表长度维护
- 动态内存的申请与释放
- 数据结构接口的边界定义
解题思路
数据结构
使用单链表实现,每个节点包含:
val:当前节点保存的值;next:指向下一个节点。
类中额外维护:
dummy_:不属于真实数据的虚拟头节点;size_:真实节点数量。
真实链表始终从 dummy_.next 开始。虚拟头节点使“在下标 0 插入”和“删除下标 0”不再需要特殊修改头指针。
找到目标节点与前驱节点
get(index) 需要找到下标为 index 的真实节点,因此从 dummy_.next 开始移动 index 次。
插入和删除需要修改目标位置前一个节点的 next,所以从 dummy_ 开始移动 index 次:
❯Text
pre → 原 index 节点插入时改为:
❯Text
pre → newNode → 原 index 节点删除时改为:
❯Text
pre ─────────→ index 后面的节点各接口逻辑
get(index)
先验证 0 <= index < size_,再从首个真实节点向后移动。
addAtIndex(index, val)
index < 0:按头部插入处理,归一化为0;index > size_:位置越过链表尾部,不执行插入;index == size_:前驱恰好是当前尾节点,插入后成为新尾节点;- 其他情况:在原下标
index节点前插入。
完成插入后执行 ++size_。
deleteAtIndex(index)
只有 0 <= index < size_ 时才删除。找到目标节点的前驱,将其绕过后释放目标节点,最后执行 --size_。
复杂度
当前实现没有额外维护尾指针:
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
get(index) | 遍历到目标节点 | |
addAtHead | 在虚拟头节点后插入 | |
addAtTail | 遍历到链表尾部 | |
addAtIndex | 遍历到目标前驱 | |
deleteAtIndex | 遍历到目标前驱 |
额外空间复杂度为 ,不计链表自身存储节点所需空间。
代码演示
❯C++
class MyLinkedList {
private:
struct ListNode {
int val;
ListNode* next;
explicit ListNode(int value = 0, ListNode* next_node = nullptr)
: val(value), next(next_node) {}
};
public:
MyLinkedList() : dummy_(0), size_(0) {}
~MyLinkedList() {
ListNode* cur = dummy_.next;
while (cur != nullptr) {
ListNode* next = cur->next;
delete cur;
cur = next;
}
}
MyLinkedList(const MyLinkedList&) = delete;
MyLinkedList& operator=(const MyLinkedList&) = delete;
int get(int index) const {
if (index < 0 || index >= size_) {
return -1;
}
const ListNode* cur = dummy_.next;
for (int i = 0; i < index; ++i) {
cur = cur->next;
}
return cur->val;
}
void addAtHead(int val) {
addAtIndex(0, val);
}
void addAtTail(int val) {
addAtIndex(size_, val);
}
void addAtIndex(int index, int val) {
if (index < 0) {
index = 0;
}
if (index > size_) {
return;
}
ListNode* prev = &dummy_;
for (int i = 0; i < index; ++i) {
prev = prev->next;
}
prev->next = new ListNode(val, prev->next);
++size_;
}
void deleteAtIndex(int index) {
if (index < 0 || index >= size_) {
return;
}
ListNode* prev = &dummy_;
for (int i = 0; i < index; ++i) {
prev = prev->next;
}
ListNode* deleted = prev->next;
prev->next = deleted->next;
delete deleted;
--size_;
}
private:
ListNode dummy_;
int size_;
};易错点
- 混淆目标节点和前驱节点:插入、删除要找到下标
index的前驱,因此从dummy_开始移动index次。 - 拒绝
index == size_的插入:该情况表示追加到尾部,是合法操作;只有index > size_才不插入。 - 删除后忘记减少长度:成功插入和删除后必须分别更新
size_。 - 无效删除仍减少长度:边界检查失败时必须直接返回。
- 覆盖丢失后续链表:创建新节点时,应先让它指向原目标节点,再让前驱指向新节点。
- 释放节点后继续访问:删除前保存连接关系,完成重连后再
delete。 - 动态分配虚拟头节点却不释放:这里直接将
dummy_作为成员对象,生命周期由类自动管理。 - 添加析构函数后保留默认浅拷贝:浅拷贝会让两个对象持有相同节点,产生重复释放;示例显式禁用了拷贝。
总结
设计链表的核心是统一位置语义:真实节点从下标 0 开始,size_ 表示可访问节点数量,而插入、删除通过虚拟头节点寻找前驱。只要明确“从哪里出发、移动几次、修改哪一条 next”,各接口就能使用一致的逻辑实现。
相关题目
-
- 移除链表元素
-
- 反转链表
-
- 删除链表的倒数第 N 个结点
-
- 设计跳表