[707] Design Linked List
1. AC代码
// @lc code=start
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;
}
/* 析构函数:释放所有节点内存,防止内存泄漏 */
~MyLinkedList()
{
ListNode *cur = _dummyHead;
while (cur != nullptr)//!!!!
{
ListNode *temp = cur;
cur = cur->next; // 最后一个节点处,cur变成nullptr,下一次循环终止
delete temp;
}
}
/* 获取第index个节点的值 */
int get(int index)
{
// 索引无效
if (index < 0 || index >= _size)
{
return -1;
}
// 从真正头节点开始遍历!
ListNode *cur = _dummyHead->next;
while (index--)
{
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 > _size)
{
return;
}
// index<0视为在头部插入
if (index < 0)
{
index = 0;
}
// 找到要插入位置的前趋节点
ListNode *pred = _dummyHead;
while (index--)
{
pred = pred->next;
}
// 插入操作
ListNode *newNode = new ListNode(val);
newNode->next = pred->next;
pred->next = newNode;
_size++;
}
/* 删除指定索引的节点 */
void deleteAtIndex(int index)
{
// 索引无效
if (index < 0 || index >= _size)
{
return;
}
// 找到要删除节点的前趋节点
ListNode *pred = _dummyHead;
while (index--)
{
pred = pred->next;
}
// 删除操作
ListNode *toDelete = pred->next;
pred->next = toDelete->next;
delete toDelete;
_size--;
}
};
/**
* Your MyLinkedList object will be instantiated and called as such:
* MyLinkedList* obj = new MyLinkedList();
* int param_1 = obj->get(index);
* obj->addAtHead(val);
* obj->addAtTail(val);
* obj->addAtIndex(index,val);
* obj->deleteAtIndex(index);
*/
// @lc code=end
2. 时空复杂度
2.1 时间复杂度 (Time Complexity)
时间复杂度衡量的是,当链表长度 N 增长时,函数执行的操作次数会如何增长。
| 函数 | 时间复杂度 | 原因分析 |
|---|---|---|
| MyLinkedList() (构造函数) | O(1) | 只执行了固定的几次操作(创建虚拟节点、赋值),与 N 无关。 |
| ~MyLinkedList() (析构函数) | O(N) | 需要遍历并 delete 掉 N+1 个节点(包括虚拟头节点)。 |
| get(int index) | O(k) (最坏 O(N)) | 需要从头遍历 k 步找到目标节点。最坏情况是 k 接近 N。 |
| addAtHead(int val) | O(1) | 直接在虚拟头节点后操作,无需遍历。 |
| addAtTail(int val) | O(N) | 需要从头遍历到链表末尾找到最后一个节点。 |
| addAtIndex(int index, int val) | O(k) (最坏 O(N)) | 需要遍历 k 步找到前驱节点。最坏情况是 k 接近 N(在尾部添加)。 |
| deleteAtIndex(int index) | O(k) (最坏 O(N)) | 需要遍历 k 步找到前驱节点。最坏情况是 k 接近 N。 |
核心结论:
- 所有与索引 index 相关的操作,以及尾部添加 addAtTail,都需要进行遍历,因此它们的最坏时间复杂度都是 O(N)。
- 只有头部添加 addAtHead,因为它得益于虚拟头节点,可以直接在头部操作,所以是 O(1)。
2.2 空间复杂度 (Space Complexity)
空间复杂度衡量的是算法执行期间所需的额外内存空间。
| 操作 | 额外空间复杂度 | 原因分析 |
|---|---|---|
| 整个 MyLinkedList 对象 | O(N) | 对象本身需要存储 N 个真实节点和1个虚拟节点,总空间与 N 成正比。 |
| 单次函数调用 | O(1) | 所有成员函数(get, add, delete)在执行时,都只使用了常数数量的额外变量(如 cur, pred, newNode 等)。它们占用的空间不随链表长度 N 的增长而增长。 |
核心结论:
- 数据结构本身的空间成本是 O(N)。
- 每一次操作的额外空间成本都是 O(1)。在面试中,如果被问到“addAtIndex 的空间复杂度是多少”,通常指的是后者,即 O(1)。
3.基础知识
3.1 while(index–) 循环的执行顺序
- 先判断: 使用 index 的当前值与 0 进行比较,来决定是否执行循环体。
- 如果 index 的当前值不为 0 (即 true),就进入循环体。
- 如果 index 的当前值为 0 (即 false),就跳出循环。
- 后自减: 在判断之后,index 的值立即减 1。这个自减操作与是否进入循环体无关,只要进行了判断,就会发生。
这个表达式其实是 C/C++ 语言的一个经典简写,它利用了两个特性:
- 后置自减 (–): index-- 这个表达式会返回 index 自减前的值。
- while 的条件判断
所以,while(index–) 实际上等价于下面这个更清晰的 while 循环:
while (index != 0) {
// ... 循环体 ...
index--;
}
结论: while(index–) 会让循环体精确地执行 index 次。
3.2 封装 (Encapsulation)
封装是面向对象编程(Object-Oriented Programming, OOP)的三大基本特性之一(另两个是继承和多态)。其核心思想是将数据(属性)和操作数据的函数(方法)捆绑成一个独立的逻辑单元,即“类”(Class),并对数据的访问权限进行严格控制,以隐藏内部实现细节,仅对外暴露有限的接口。
class MyLinkedList {
// ... 类的成员 ...
}; // 注意:类定义末尾必须有分号 ;
实现机制:访问说明符 (Access Specifiers)
C++通过三个关键字——public, private, protected——来实现封装,它们定义了类成员的访问级别。
- public成员可以被程序中的任何部分访问,构成类的公共接口 (Public Interface)。这是类与外部世界交互的唯一合法途径。
- private成员只能被其所属类的成员函数以及该类的友元 (friend) 访问。派生类和外部代码都无法直接访问。
- protected专门为继承体系设计。它允许子类继承并访问父类的某些内部状态或辅助函数,但同时又对外部世界保持隐藏。
3.3 私有成员变量以 _ 开头
为了在代码中一眼就能区分出哪些是类的成员变量,哪些是局部变量或函数参数,我们用了下划线约定。
// 如果不用下划线
MyLinkedList(int size) {
size = size; // 这里的两个 size 指的都是函数参数!成员变量没有被赋值!
}
// 为了解决,需要用 this 指针
MyLinkedList(int size) {
this->size = size; // 这样才能明确区分
}
MyLinkedList(int size) {
_size = size; // 非常清晰,左边是成员变量,右边是参数
}
3.4 嵌套类型
在一个类(或结构体)的内部,定义另一个新的类、结构体或枚举,这个新的类型就被称为嵌套类型。struct ListNode 是在 MyLinkedList 类内部定义的一个新的、嵌套的数据类型。这样做既能清晰地表达 ListNode 是 MyLinkedList 的一个专属实现细节,又能有效地避免与外部代码的命名冲突。
1090

被折叠的 条评论
为什么被折叠?



