好的,这是一份关于 C++ STL list 容器底层实现的详细笔记。


STL list 容器详解

STL 中的 list 是一个双向链表。与 vector 和 deque 不同,它不是连续存储的线性结构,而是由一系列在内存中不一定连续存放的节点组成。每个节点包含数据和指向其前驱节点和后继节点的指针。这种结构使得 list 在任意位置的插入和删除操作非常高效,时间复杂度为 $O(1)$,但随机访问效率较低,时间复杂度为 $O(n)$。

核心数据结构:节点 _Node

list 的实现核心是一个表示节点的结构体或类模板。每个节点通常包含三个部分:

  1. 数据域:存储用户提供的元素值。
  2. 前驱指针:指向链表中前一个节点。
  3. 后继指针:指向链表中后一个节点。
template <typename _Tp>
struct _List_node {
    _List_node* _M_next;  // 后继指针
    _List_node* _M_prev;  // 前驱指针
    _Tp _M_data;          // 存储的数据
};

list 类模板

list 本身是一个类模板,主要包含以下关键成员:

  1. 头节点指针 _M_head:

    • 通常指向一个特殊的节点,称为哨兵节点或头节点。
    • 这个节点本身不存储有效数据。
    • 它的 _M_next 指向链表的第一个有效节点(如果链表为空,则指向自身)。
    • 它的 _M_prev 指向链表的最后一个有效节点(如果链表为空,则指向自身)。
    • 使用哨兵节点可以简化边界条件(如空链表)的处理,并使得 begin() 和 end() 的实现更清晰。
  2. 节点分配器 _M_get_Node_allocator():

    • list 使用分配器来管理节点的内存分配和释放。
    • 分配器通常作为模板参数传入,默认使用 std::allocator。
迭代器 iterator

list 的迭代器是一个双向迭代器。它封装了一个指向 _List_node 的指针,并重载了相应的运算符(如 ++, --, *, ->)来模拟指针行为。

template <typename _Tp>
struct _List_iterator {
    _List_node_base* _M_node; // 通常指向一个_List_node_base(包含指针部分)
    // 运算符重载
    _Tp& operator*() const { return static_cast<_List_node<_Tp>*>(_M_node)->_M_data; }
    _List_iterator& operator++() { _M_node = _M_node->_M_next; return *this; }
    // ... 其他运算符重载
};

  • begin():返回指向第一个有效节点的迭代器(即 _M_head->_M_next)。
  • end():返回指向哨兵节点的迭代器(即 _M_head)。它不指向有效数据,代表末尾的下一个位置。
关键操作实现
  1. 插入操作 insert:

    • 在指定位置 pos 前插入新元素。
    • 创建一个新节点 __new_node。
    • 调整指针:
      __new_node->_M_next = __pos._M_node;          // 新节点指向pos位置的节点
      __new_node->_M_prev = __pos._M_node->_M_prev; // 新节点指向pos的前驱
      __pos._M_node->_M_prev->_M_next = __new_node; // pos前驱的后继指向新节点
      __pos._M_node->_M_prev = __new_node;          // pos的前驱指向新节点
      

    • 时间复杂度:$O(1)$。
  2. 删除操作 erase:

    • 删除指定位置 pos 的元素。
    • 调整指针:
      __pos._M_node->_M_prev->_M_next = __pos._M_node->_M_next; // pos前驱的后继指向pos的后继
      __pos._M_node->_M_next->_M_prev = __pos._M_node->_M_prev; // pos后继的前驱指向pos的前驱
      

    • 释放 __pos._M_node 的内存。
    • 时间复杂度:$O(1)$。
  3. push_back / push_front:

    • 本质上是分别在 end() 和 begin() 前插入元素。直接调用 insert 即可。
    • 时间复杂度:$O(1)$。
  4. pop_back / pop_front:

    • 本质上是删除 end() 的前一个元素和 begin() 位置的元素。直接调用 erase 即可。
    • 时间复杂度:$O(1)$。
  5. size():

    • 标准库实现通常会维护一个 size 成员变量,在插入和删除时更新它,使得 size() 可以在 $O(1)$ 时间内完成。
    • 部分实现(或早期实现)可能选择遍历链表计数,时间复杂度 $O(n)$。
  6. splice:

    • 将另一个链表的一部分或全部移动到当前链表的指定位置。
    • 核心操作是调整相关节点的指针,将源链表中的节点链入目标位置,并断开其在原链表中的连接。
    • 时间复杂度:$O(1)$ 或 $O(n)$,取决于移动的范围。
内存管理
  • 节点的内存分配和释放由分配器负责。
  • 分配器通常使用 new 和 delete,但也可以配置为使用内存池等优化策略。
  • 在 clear() 或析构函数中,需要遍历链表,逐个释放所有节点。
简单实现示例 (简化版)
template <typename T>
class List {
private:
    struct Node {
        Node* next;
        Node* prev;
        T data;
        Node(const T& val = T(), Node* n = nullptr, Node* p = nullptr)
            : data(val), next(n), prev(p) {}
    };

    Node* head; // 哨兵节点
    size_t count; // 元素个数计数器

public:
    List() : head(new Node()), count(0) {
        head->next = head;
        head->prev = head;
    }
    ~List() {
        clear();
        delete head;
    }

    void push_back(const T& value) {
        Node* newNode = new Node(value, head, head->prev);
        head->prev->next = newNode;
        head->prev = newNode;
        ++count;
    }

    void pop_back() {
        if (count == 0) return;
        Node* last = head->prev;
        last->prev->next = head;
        head->prev = last->prev;
        delete last;
        --count;
    }

    void clear() {
        Node* cur = head->next;
        while (cur != head) {
            Node* next = cur->next;
            delete cur;
            cur = next;
        }
        head->next = head;
        head->prev = head;
        count = 0;
    }

    // ... 其他成员函数 (begin, end, insert, erase, size 等)
};
总结
  • 结构:双向链表,节点包含数据、前驱指针、后继指针。
  • 哨兵节点:简化边界处理,begin() 指向第一个节点,end() 指向哨兵节点。
  • 迭代器:双向迭代器,封装节点指针。
  • 插入/删除:核心是调整节点指针,时间复杂度 $O(1)$。
  • 随机访问:效率低,$O(n)$。
  • 内存:非连续存储,节点动态分配。
  • 适用场景:需要频繁在任意位置插入和删除元素,不需要随机访问。

理解 list 的底层实现有助于更好地运用它,并明白其与 vector、deque 在性能特性上的根本区别。

更多推荐