[707] Design Linked List 设计链表

[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–) 循环的执行顺序
  1. 先判断: 使用 index 的当前值与 0 进行比较,来决定是否执行循环体。
    • 如果 index 的当前值不为 0 (即 true),就进入循环体。
    • 如果 index 的当前值为 0 (即 false),就跳出循环。
  2. 后自减: 在判断之后,index 的值立即减 1。这个自减操作与是否进入循环体无关,只要进行了判断,就会发生。

这个表达式其实是 C/C++ 语言的一个经典简写,它利用了两个特性:

  1. 后置自减 (–): index-- 这个表达式会返回 index 自减前的值。
  2. 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 的一个专属实现细节,又能有效地避免与外部代码的命名冲突。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值