认识STL序列式容器——List

目录

🔁一、list的底层结构------双向链表

1️⃣节点结构

2️⃣链表的布局

3️⃣list的begin()和end()

🔎二、list迭代器:双向访问的实现

1️⃣迭代器的底层实现

2️⃣迭代器的失效规则

📈三、list的操作与性能

1️⃣元素插入(O(1)复杂度优势)

①头部插入(push_front)与尾部插入(push_back)

②任意位置插入(insert)

2️⃣元素删除(O(1)复杂度优势)

①头部删除(pop_front)与尾部删除(pop_back)

②任意位置删除(erase)

3️⃣元素访问的劣势

4️⃣其他操作

⚔️四、list和vector的核心差异

⚠️五、list使用需注意的事项

1️⃣避免频繁的随机访问

2️⃣迭代器失效的特殊情况

3️⃣对splice()的了解

📚六、总结

在C++STL的序列式容器中,list是一个独特的存在------它是基于双向链表实现,与vector的连续内存空间形成鲜明对比。


🔁一、list的底层结构------双向链表

list 的核心是双向链表 ,这意味着它的元素在内存中非连续存储 ,并且通过指针相互连接。

1️⃣节点结构

list 的每个元素都被封装在一个 "节点(node)" 中,节点间通过指针链接。GCC 标准库中节点的核心定义如下(简化版):

cpp 复制代码
template <typename T>
struct _List_node {
    typedef _List_node<T>* pointer;
    pointer _M_next;  // 指向后一个节点的指针
    pointer _M_prev;  // 指向前一个节点的指针
    T _M_data;        // 存储的元素数据
};
  • 每个节点都包含三个部分:前驱指针(_M_pprev)、后继指针(_M_next)、元素数据(_M_data
  • 链表的 "头节点" 通常是一个哨兵节点(sentinel) ,不存储实际数据,仅用于简化边界操作(如头插、尾插的逻辑统一)

2️⃣链表的布局

如果链表为空,那么头节点的 prevnext 都指向自己,形成一个环。

  • 哨兵节点的存在让链表的 "空状态" 和 "非空状态" 处理逻辑一致(避免判断 nullptr
  • 双向循环特性使头插、尾插、头删、尾删的操作复杂度均为 O (1)

3️⃣listbegin()end()

list 容器本身持有指向哨兵节点的指针,以及分配器(管理节点内存):

cpp 复制代码
template <typename T, typename Alloc = allocator<T>>
class list {
private:
    _List_node<T>* _M_node;  // 指向哨兵节点的指针
    allocator_type _M_get_allocator() const;  // 节点内存分配器
public:
    // 迭代器相关(见下文)
    iterator begin() { return _M_node->_M_next; }
    iterator end() { return _M_node; }  // end() 指向哨兵节点
    // ... 其他接口
};
  • begin() 返回第一个元素的迭代器(哨兵节点的 _M_next)。
  • end() 返回哨兵节点的迭代器。

🔎二、list迭代器:双向访问的实现

list 的迭代器是双向迭代器 ,支持 ++(向后移动)和 --(向前移动),但不支持随机访问(就比如 it + 5 是不允许的)。

1️⃣迭代器的底层实现

迭代器本质是封装了节点指针的对象,通过重载 *->++-- 等运算符实现元素访问和移动:

cpp 复制代码
template <typename T>
struct _List_iterator {
    typedef _List_node<T>* pointer;
    pointer _M_node;  // 指向当前节点的指针

    // 解引用:获取元素数据
    reference operator*() const { return _M_node->_M_data; }
    // 箭头运算符:访问元素成员
    pointer operator->() const { return &(operator*()); }

    // 前置++:移动到下一个节点
    _List_iterator& operator++() {
        _M_node = _M_node->_M_next;
        return *this;
    }
    // 前置--:移动到上一个节点
    _List_iterator& operator--() {
        _M_node = _M_node->_M_prev;
        return *this;
    }
};
  • 迭代器的 ++-- 操作本质是修改节点指针(_M_next/_M_prev),时间复杂度 O (1)。
  • 不支持随机访问的原因:链表元素并不是连续的,无法通过指针偏移直接定位(比如 it + n 需要遍历 n 个节点,复杂度 O (n))。

2️⃣迭代器的失效规则

vector 扩容导致大规模迭代器失效不同,list 的迭代器失效规则更简单:

  • 删除元素时:仅指向被删除节点的迭代器失效,其他迭代器(包括被删除节点的前驱、后继)仍有效。
  • 插入元素时:所有迭代器均有效(因为插入不会影响已有节点的指针)。

这是链表结构的显著优势 ------ 插入 / 删除操作对其他节点无影响,仅需调整相邻节点的指针


📈三、list的操作与性能

1️⃣元素插入(O(1)复杂度优势)

list 支持在任意位置插入元素,且时间复杂度均为 O (1)(只需调整指针,无需移动元素)。

①头部插入(push_front)与尾部插入(push_back
cpp 复制代码
list<int> list;
list.push_back(10);
list.push_front(9);
  • 头插:
    • 仅仅需要调整哨兵节点的_M_next和原来头结点的指针
  • 尾插:
    • 建新节点,数据为 10
    • 将新节点的 _M_prev 指向原尾节点
    • 将原尾节点的 _M_next 指向新节点
    • 将哨兵节点的 _M_prev 指向新节点
②任意位置插入(insert
cpp 复制代码
auto it = list.begin();  // 指向第一个元素
list.insert(it, 30);     // 在 it 前插入 30
  • 底层逻辑:
    • 创建新节点,数据为 30
    • 新节点的 _M_prev = it 节点的 _M_prev(就是新节点的_M_prev指向it节点的_M_prev所指向的节点)
    • 新节点的 _M_next = it 节点
    • 调整 it 节点的前一个结点的 _M_next 指向新节点
    • 调整 it 节点的 _M_prev 指向新节点
  • 无论插入位置在哪里(头部、中间、尾部),均只需修改 4 个指针,复杂度 O (1)(前提是已获取插入位置的迭代器)。

2️⃣元素删除(O(1)复杂度优势)

list 的删除操作同样只需调整指针,无需移动元素,时间复杂度 O (1)。

①头部删除(pop_front)与尾部删除(pop_back
cpp 复制代码
list.pop_front();
list.pop_back();
  • 尾删底层逻辑
    • 获取原尾节点(哨兵节点的 _M_prev
    • 将哨兵节点的 _M_prev 指向原尾节点的 _M_prev(更新尾节点)
    • 将新尾节点的 _M_next 指向哨兵节点
    • 销毁原尾节点的元素,释放节点内存
②任意位置删除(erase
cpp 复制代码
auto it = list.begin();
it = list.erase(it);  // 删除 it 指向的元素,返回下一个元素的迭代器
  • 底层逻辑:
    • 获取待删除节点的前驱(p = it->_M_prev)和后继(n = it->_M_next)
    • 调整 p->_M_next = n 和 n->_M_prev = p(跳过待删除节点)
    • 销毁待删除节点的元素,释放节点内存
    • 返回指向 n 的迭代器(避免原迭代器失效)

3️⃣元素访问的劣势

list不支持operator[]at()和随机访问,访问第n个元素必须从头部或者是尾部开始遍历,时间复杂度O(n)

这是list最显著的劣势------不适合需要频繁随机访问的场景

4️⃣其他操作

  • clear():销毁所有元素,释放节点内存,但保留哨兵节点,size() 变为 0。
  • swap():交换两个 list 的哨兵节点指针,O (1) 复杂度(与 vector 相同)。
  • splice():将一个 list 的部分或全部元素转移到另一个 list,仅调整指针,O (1) 复杂度(list 特有的高效操作)。

⚔️四、list和vector的核心差异

|-------------|---------------------|-------------------------|
| 特性 | list(双向链表) | vector(动态数组) |
| 内存布局 | 非连续,节点通过指针链接 | 连续内存块 |
| 随机访问 | 不支持(O (n)) | 支持(O (1)) |
| 插入 / 删除(中间) | O (1)(仅调整指针) | O (n)(需移动元素) |
| 插入 / 删除(尾部) | O(1) | O (1)(无扩容时)/ O (n)(扩容时) |
| 迭代器失效规则 | 仅被删除元素的迭代器失效 | 扩容时所有失效,插入 / 删除中间时部分失效 |
| 内存利用率 | 有额外指针开销(每个节点 2 个指针) | 连续内存,无指针开销,但可能有预留空间浪费 |
| 缓存友好性 | 差(元素分散,缓存命中率低) | 好(连续内存,缓存命中率高) |

  • 若需频繁随机访问 (如 v[i]),选 vector
  • 若需频繁在中间插入 / 删除 ,选 list
  • 若元素体积大 (如大对象),list 的插入 / 删除优势更明显(避免 vector 的大量拷贝)
  • 若操作以尾部增删 为主,vector 更优(缓存友好,无指针开销)

⚠️五、list使用需注意的事项

1️⃣避免频繁的随机访问

比如,遍历list的时候使用迭代器来移动(++it)是O(n),但是通过下标访问每个元素会变成O(n2)

cpp 复制代码
// 模拟下标访问 - O(n²)
for (size_t i = 0; i < lst.size(); ++i) 
{
    auto it = lst.begin();
    std::advance(it, i);  // 每次从头开始移动 i 步
    std::cout << *it << " ";
}

2️⃣迭代器失效的特殊情况

删除元素后,仅被删除的迭代器失效 ,其他迭代器仍可以使用

cpp 复制代码
list<int> list = {1,2,3,4};
auto it = list.begin();
++it;  // 指向 2
list.erase(l.begin());  // 删除 1,it 仍指向 2(有效)

插入元素后,所有迭代器都有效(包括插入位置的迭代器)

cpp 复制代码
auto it = list.begin();
list.insert(it, 10);  // 插入后 it 仍指向原元素(有效)

3️⃣对splice()的了解

listsplice() 是独特的高效操作,可在 O (1) 时间内转移元素(无需拷贝,仅调整指针),适合批量元素移动:

cpp 复制代码
list<int> l1 = {1,2,3};
list<int> l2 = {4,5,6};

// 将 l2 的所有元素移到 l1 末尾
l1.splice(l1.end(), l2);  // l1 = {1,2,3,4,5,6},l2 为空

📚六、总结

list 凭借双向链表的特性,在任意位置插入 / 删除 场景中展现出 O (1) 复杂度的优势,但也因为非连续内存 导致随机访问能力缺失、缓存友好性差。使用时需根据具体场景权衡:频繁中间操作选 list,频繁随机访问选 vector

相关推荐
醇氧5 小时前
【Windows】优雅启动:解析一个 Java 服务的后台启动脚本
java·开发语言·windows
hetao17338375 小时前
2025-12-12~14 hetao1733837的刷题笔记
数据结构·c++·笔记·算法
椰子今天很可爱5 小时前
五种I/O模型与多路转接
linux·c语言·c++
MapGIS技术支持5 小时前
MapGIS Objects Java计算一个三维点到平面的距离
java·开发语言·平面·制图·mapgis
程序员zgh6 小时前
C++ 互斥锁、读写锁、原子操作、条件变量
c语言·开发语言·jvm·c++
小灰灰搞电子6 小时前
Qt 重写QRadioButton实现动态radioButton源码分享
开发语言·qt·命令模式
by__csdn6 小时前
Vue3 setup()函数终极攻略:从入门到精通
开发语言·前端·javascript·vue.js·性能优化·typescript·ecmascript
喵了meme6 小时前
C语言实战5
c语言·开发语言
廋到被风吹走7 小时前
【Java】常用设计模式及应用场景详解
java·开发语言·设计模式
Sammyyyyy7 小时前
DeepSeek v3.2 正式发布,对标 GPT-5
开发语言·人工智能·gpt·算法·servbay