当前位置: 首页 > ai >正文

序列容器(vector,deque,list)

STL 序列式容器(vectordequelistarrayforward_list)的核心特征是按插入顺序存储元素(元素的逻辑顺序与物理存储顺序一致)

 vector

下图是底层原理 具体点击链接vector介绍

deque(双端队列)

在 C++ STL 中,deque(Double-Ended Queue,双端队列)是一种支持两端高效插入 / 删除的序列式容器,底层通过 “分段动态数组 + 中控数组” 的结构实现,兼顾了随机访问能力和双端操作效率。以下从核心特性、底层原理、常用接口、适用场景等方面全面解析 deque

一、核心特性

  1. 双端高效操作

    • 头部(front)和尾部(back)的插入(push_front/push_back)、删除(pop_front/pop_back)操作均为 O (1) 时间复杂度(无需像 vector 那样移动大量元素,也无需像 list 那样分配节点)。
  2. 支持随机访问

    • 提供下标运算符([])和 at() 成员函数,可直接访问指定位置的元素,时间复杂度 O(1)(优于 list 的 O (n),但底层实现比 vector 复杂)。
  3. 动态扩容

    • 容量不足时会自动扩容,但扩容逻辑比 vector 更灵活:vector 扩容需分配新的连续内存并拷贝所有元素(O (n) 开销),而 deque 只需新增一个 “分段数组” 并更新中控数组(避免大规模拷贝,平均扩容开销更低)。
  4. 非连续内存存储

    • 底层并非连续内存(与 vector 不同),而是由多个小型连续内存块(分段数组)和一个 “中控数组”(存储各分段数组的地址)组成,通过中控数组实现对分段的管理,对外表现为 “连续的元素序列”。

二、底层原理(为什么能兼顾双端操作和随机访问?)

deque 的底层结构可拆解为两部分,理解这部分能更清晰其特性来源:

  1. 分段数组(Buffer)

    • 每个分段数组是固定大小的连续内存块(例如默认 512 字节),用于存储实际的元素;
    • 分段数组的大小可通过编译器宏(如 _DEQUE_BUF_SIZE)调整,也可在定义 deque 时通过模板参数指定。
  2. 中控数组(Map)

    • 本质是一个 “指针数组”,每个元素指向一个分段数组;
    • 中控数组会预留一定的空闲位置(头部和尾部),当头部需要插入元素且当前首分段已满时,直接在中控数组头部新增一个分段指针;尾部同理,避免中控数组频繁扩容。

访问元素的逻辑
当通过下标 i 访问 deque 元素时,底层会先计算 i 所在的 “分段索引”(i / 分段大小)和 “分段内偏移”(i % 分段大小),再通过中控数组找到对应的分段数组,最后通过偏移获取元素 —— 这一过程是 O (1),因此支持随机访问。

三、常用成员函数(核心接口)

deque 的接口与 vector 类似,但新增了双端操作的接口,以下是高频使用的成员函数:

类别成员函数功能描述
构造 / 析构deque()默认构造空的 deque
deque(n, val)构造包含 n 个 val 的 deque
deque(beg, end)从迭代器范围 [beg, end) 构造 deque(如从 vector 或 string 初始化)
双端操作push_front(val)在头部插入元素 val(O(1))
push_back(val)在尾部插入元素 val(O(1))
pop_front()删除头部元素(O (1),需确保 deque 非空)
pop_back()删除尾部元素(O (1),需确保 deque 非空)
元素访问operator[](i)访问下标 i 处的元素(无越界检查,越界行为未定义)
at(i)访问下标 i 处的元素(有越界检查,越界抛 out_of_range 异常)
front()获取头部元素(O (1))
back()获取尾部元素(O (1))
容量操作size()返回元素个数(O (1))
empty()判断是否为空(O (1),空返回 true
resize(n, val)调整元素个数为 n:不足补 val,多余删除
clear()清空所有元素(O (n),元素析构,但中控数组和分段数组可能保留以复用)
迭代器begin()/end()获取首元素 / 尾后元素的迭代器(支持随机访问,可 ++/--/+n/-n
rbegin()/rend()获取反向迭代器(从尾部向头部遍历)

四、适用场景(什么时候用 deque?)

deque 的特性决定了它适合 “需要双端操作 + 随机访问” 的场景,典型场景包括:

  1. 实现队列(Queue)或双端队列

    • STL 的 queue 容器适配器默认以 deque 为底层容器(因为 queue 需支持 push_back 和 pop_frontdeque 这两个操作都是 O (1),优于 vector 的 pop_front(O(n)))。
  2. 频繁在两端插入 / 删除,且需要随机访问

    • 例如:滑动窗口算法(需在窗口两端添加 / 移除元素,同时访问窗口内任意位置元素)、日志存储(新日志追加到尾部,旧日志从头部删除,偶尔需查询中间日志)。
  3. 替代 vector 避免大规模扩容开销

    • 当 vector 存储大量元素时,扩容会拷贝所有元素,开销较大;而 deque 扩容只需新增分段,适合元素数量动态增长且规模较大的场景(但随机访问效率略低于 vector,需权衡)。

五、与其他容器的对比(vector vs list vs deque

特性vectorlistdeque
底层结构连续动态数组双向链表分段数组 + 中控数组
随机访问([]/at支持(O (1))不支持(O (n))支持(O (1))
头部插入 / 删除不支持头部插入,可以删除高效(O (1))高效(O (1))
尾部插入 / 删除高效(O (1),扩容时 O (n))高效(O (1))高效(O (1))
中间插入 / 删除不高效(O (n))高效(O (1),找位置 O (n))不高效(O (n))
内存利用率高(连续内存,缓存友好)低(节点有指针开销)中等(分段有少量浪费)
扩容开销高(拷贝所有元素)低(仅分配新节点)低(新增分段)

六、使用示例(代码演示)

#include <iostream>
#include <deque>
#include <algorithm>  // 用于 sort 等算法int main() {// 1. 构造 deque 并初始化std::deque<int> dq1;                          // 空 dequestd::deque<int> dq2(5, 10);                   // 5个10:[10,10,10,10,10]std::deque<int> dq3(dq2.begin(), dq2.end());  // 从 dq2 复制:[10,10,10,10,10]// 2. 双端插入元素dq1.push_back(20);   // 尾部插入:[20]dq1.push_front(10);  // 头部插入:[10,20]dq1.push_back(30);   // 尾部插入:[10,20,30]// 3. 随机访问和遍历std::cout << "dq1[1] = " << dq1[1] << std::endl;  // 输出 20(随机访问)std::cout << "dq1 遍历:";for (int i = 0; i < dq1.size(); ++i) {std::cout << dq1[i] << " ";  // 10 20 30}std::cout << std::endl;// 4. 双端删除元素dq1.pop_front();  // 删除头部:[20,30]dq1.pop_back();   // 删除尾部:[20]// 5. 排序(支持随机访问迭代器,可直接用 sort 算法)std::deque<int> dq4 = {3, 1, 4, 2};std::sort(dq4.begin(), dq4.end());  // 排序后:[1,2,3,4]std::cout << "排序后 dq4:";for (auto val : dq4) {std::cout << val << " ";  // 1 2 3 4}return 0;
}

七、注意事项

  1. 避免中间插入 / 删除deque 中间插入 / 删除需要移动该位置两侧的元素(O (n) 时间),效率低,若需频繁中间操作,优先用 list
  2. 迭代器稳定性deque 的迭代器在扩容时可能失效(中控数组扩容会导致分段指针地址变化),但在两端插入 / 删除时,除了首分段和尾分段的迭代器,其他分段的迭代器仍有效(与 vector 所有迭代器失效不同)。
  3. 不适合存储大对象:每个分段数组存储元素,若元素体积大,分段数量会减少,但随机访问逻辑不变,不过内存碎片可能增加(需结合实际场景权衡)。

总之,deque 是 STL 中 “平衡双端操作和随机访问” 的容器,在需要频繁操作两端且需随机访问的场景中,是比 vector 和 list 更优的选择。

list(双向链表)

在 C++ STL 中,std::list 是一种双向链表(双向非循环链表) 容器,其元素通过节点间的指针连接,不要求内存连续。这种结构使其在任意位置的插入和删除操作上具有独特优势,以下从核心特性、底层原理、常用接口和适用场景等方面详细解析:

一、核心特性

  1. 双向链表结构
    每个节点包含:

    • 数据域(存储元素值)
    • 前驱指针(指向当前节点的前一个节点)
    • 后继指针(指向当前节点的后一个节点)
      首节点的前驱指针和尾节点的后继指针均为 nullptr,不形成循环。
  2. 高效的插入与删除

    • 在任意位置(头部、尾部、中间)插入或删除元素的时间复杂度为 O(1)(只需修改相邻节点的指针,无需移动其他元素)。
    • 但查找目标位置需要遍历链表,时间复杂度为 O(n),因此 “查找 + 插入 / 删除” 的整体复杂度为 O (n)。
  3. 不支持随机访问
    无法通过下标([])直接访问元素,必须从首节点或尾节点开始遍历(通过迭代器 ++/-- 移动),访问第 n 个元素的时间复杂度为 O (n)(这是与 vectordeque 的核心区别)。

  4. 内存动态分配
    元素在堆上逐个分配内存,节点间无需连续,因此不会像 vector 那样因扩容导致大规模内存拷贝,但存在额外的指针内存开销(每个节点两个指针)。

二、底层原理(为何插入删除高效?)

list 的高效插入 / 删除源于其链表结构:

  • 当已知插入 / 删除位置的迭代器时,操作仅需 3 步:
    1. 创建新节点(插入时)或标记待删除节点(删除时);
    2. 修改前一个节点的后继指针,指向新节点(插入)或下一个节点(删除);
    3. 修改新节点(插入)或下一个节点的前驱指针,指向合适的节点。
  • 整个过程无需移动其他元素,仅涉及指针修改,因此效率极高。

例如,在节点 A 和 B 之间插入节点 C

原链表:A <-> B  
插入后:A <-> C <-> B  
(仅需修改 A 的后继指针和 B 的前驱指针)

三、常用成员函数(核心接口)

list 提供了丰富的接口,尤其针对链表特性优化了插入、删除和移动操作:

类别成员函数功能描述
构造 / 析构list()默认构造空链表
list(n, val)构造包含 n 个 val 的链表
list(beg, end)从迭代器范围 [beg, end) 构造链表(如从 vector 或数组初始化)
两端操作push_front(val)在头部插入元素(O (1))
push_back(val)在尾部插入元素(O (1))
pop_front()删除头部元素(O (1),需确保链表非空)
pop_back()删除尾部元素(O (1),需确保链表非空)
任意位置操作insert(pos, val)在迭代器 pos 指向的位置前插入 val,返回新元素的迭代器(O (1))
erase(pos)删除迭代器 pos 指向的元素,返回下一个元素的迭代器(O (1))
erase(beg, end)删除迭代器范围 [beg, end) 内的元素,返回下一个元素的迭代器(O (n))
元素访问front()获取头部元素的引用(O (1))
back()获取尾部元素的引用(O (1))
链表操作clear()清空所有元素(O (n))
size()返回元素个数(O (1),C++11 后支持,之前版本需遍历计数)
empty()判断是否为空(O (1))
sort()对链表排序(内部实现为归并排序,O (n log n),不支持 std::sort 因为需要随机访问迭代器)
reverse()反转链表(O (n))
splice(pos, list2)将链表 list2 的所有元素移动到当前链表 pos 位置前(O (1),无元素拷贝)

四、适用场景(什么时候用 list?)

list 适合以下场景:

  1. 频繁在任意位置插入 / 删除元素
    例如:实现链表式数据结构(如邻接表)、日志系统(需在中间插入修正记录)、 undo/redo 操作栈(频繁在头部插入历史记录)。

  2. 元素数量动态变化大,且无需随机访问
    当不需要通过下标访问元素,仅需顺序遍历,且增删操作频繁时,list 比 vector(中间增删效率低)和 deque(中间增删效率低)更合适。

  3. 需要高效移动元素块
    通过 splice 成员函数可以 O (1) 时间移动整个链表或子链表(无元素拷贝,仅修改指针),适合实现链表合并、拆分等操作。

五、与 vectordeque 的对比

特性listvectordeque
底层结构双向链表连续动态数组分段数组 + 中控数组
随机访问([]不支持(O (n))支持(O (1))支持(O (1))
任意位置插入 / 删除O (1)(已知位置)O (n)(需移动元素)O (n)(需移动元素)
内存连续性不连续连续分段连续
内存开销高(节点含指针)低(仅存储元素)中(分段管理)
迭代器类型双向迭代器随机访问迭代器随机访问迭代器
适用场景频繁任意位置增删频繁随机访问频繁双端操作 + 随机访问

六、使用示例(代码演示)

#include <iostream>
#include <list>
#include <algorithm>  // 用于 find 等算法int main() {// 1. 构造 list 并初始化std::list<int> lst1;                      // 空链表std::list<int> lst2(3, 5);                // 3个5:[5,5,5]std::list<int> lst3(lst2.begin(), lst2.end());  // 从 lst2 复制:[5,5,5]// 2. 插入元素lst1.push_back(10);    // 尾部插入:[10]lst1.push_front(20);   // 头部插入:[20,10]auto it = lst1.begin();  // it 指向 20++it;                    // it 指向 10lst1.insert(it, 30);    // 在 10 前插入 30:[20,30,10]// 3. 遍历元素(只能通过迭代器或范围 for)std::cout << "lst1 元素:";for (int val : lst1) {std::cout << val << " ";  // 20 30 10}std::cout << std::endl;// 4. 删除元素lst1.pop_front();       // 删除头部:[30,10]it = std::find(lst1.begin(), lst1.end(), 30);  // 查找 30 的迭代器if (it != lst1.end()) {lst1.erase(it);     // 删除 30:[10]}// 5. 链表特有操作std::list<int> lst4 = {3, 1, 2};lst4.sort();            // 排序:[1,2,3]lst4.reverse();         // 反转:[3,2,1]std::cout << "lst4 排序反转后:";for (int val : lst4) {std::cout << val << " ";  // 3 2 1}return 0;
}

七、注意事项

  1. 迭代器有效性list 的迭代器在插入元素后仍有效(只需调整指针,节点地址不变);删除元素后,只有指向被删除节点的迭代器失效,其他迭代器仍可使用(这是 list 的重要优势)。
  2. 不支持 std::sortstd::sort 要求随机访问迭代器,而 list 是双向迭代器,因此必须使用自身的 sort() 成员函数。
  3. 缓存不友好:由于元素内存不连续,CPU 缓存命中率低,遍历速度通常慢于 vector(即使两者时间复杂度均为 O (n))。

总之,list 是针对 “频繁任意位置增删” 优化的容器,在不需要随机访问的场景中,能显著提升操作效率。

---------------------------------------------------------------------------------------------------------------------------------

deque插入(insert)的底层原理

通过__index判断插入点位置,选择移动元素更少的方向(头部或尾部),体现 “短路径优先” 的优化思想。这段代码的注释清晰展现了deque中间插入的优化逻辑,即通过巧妙利用头尾操作的高效性,减少元素移动的数量,从而提升插入性能。

// 模板函数定义:在deque的中间位置插入单个元素的辅助函数
// 返回值:插入后新元素的迭代器
// 参数:__pos-插入位置迭代器;__x-要插入的元素值
template <class _Tp, class _Alloc>
typename deque<_Tp, _Alloc>::iterator
deque<_Tp,_Alloc>::_M_insert_aux(iterator __pos, const value_type& __x)
{// 计算插入点__pos相对于起始位置_M_start的偏移量(逻辑索引)// 用于判断插入点更靠近头部还是尾部difference_type __index = __pos - _M_start;// 复制要插入的元素(避免原元素被意外修改,确保值的稳定性)value_type __x_copy = __x;// 判断插入点是否更靠近头部(距离头部比距离尾部近)// 采用"短路径优先"策略,减少需要移动的元素数量if (size_type(__index) < this->size() / 2) {// 分支1:插入点靠近头部,从头部方向移动元素// 在头部插入一个当前头部元素的副本(利用deque头部插入高效的特性)// 此时头部会有一个重复元素,后续会通过移动覆盖push_front(front());// 定义迭代器:__front1指向新插入的头部副本元素iterator __front1 = _M_start;++__front1;// __front2指向__front1的下一个元素(原头部元素)iterator __front2 = __front1;++__front2;// 重新定位插入点__pos:因头部插入可能导致迭代器失效,通过索引重新计算__pos = _M_start + __index;// __pos1指向插入点的下一个元素(需要移动的起始位置)iterator __pos1 = __pos;++__pos1;// 将[__front2, __pos1)范围内的元素复制到__front1开始的位置// 效果:从头部方向将元素向后移动一个位置,为新元素腾出空间copy(__front2, __pos1, __front1);}else {// 分支2:插入点靠近尾部,从尾部方向移动元素// 在尾部插入一个当前尾部元素的副本(利用deque尾部插入高效的特性)// 此时尾部会有一个重复元素,后续会通过移动覆盖push_back(back());// 定义迭代器:__back1指向新插入的尾部副本元素iterator __back1 = _M_finish;--__back1;// __back2指向__back1的前一个元素(原尾部元素)iterator __back2 = __back1;--__back2;// 重新定位插入点__pos:因尾部插入可能导致迭代器失效,通过索引重新计算__pos = _M_start + __index;// 将[__pos, __back2]范围内的元素从后向前复制到__back1方向// 效果:从尾部方向将元素向后移动一个位置,为新元素腾出空间copy_backward(__pos, __back2, __back1);}// 在腾出的插入位置放入新元素的副本*__pos = __x_copy;// 返回指向新插入元素的迭代器return __pos;
}

需要注意的是: vector 的 insert 操作可能导致迭代器失效,这是由 vector 底层连续内存的特性决定的。当插入元素引发内存重新分配或元素移动时,原有的迭代器、指针或引用可能指向无效内存,使用它们会导致未定义行为(如程序崩溃、数据错误)。

如何避免迭代器失效?

  1. 使用 insert 的返回值更新迭代器
    vector::insert 会返回一个新的迭代器,指向插入的新元素。用这个返回值更新原迭代器,可避免失效:

    auto it = vec.begin() + 1;
    // 插入元素并获取新迭代器
    it = vec.insert(it, 99);  // it 现在指向新插入的99,有效
    
  2. 提前预留足够容量(reserve
    若已知插入元素的数量,先用 reserve(n) 预留足够容量,避免插入时触发扩容:

    vec.reserve(100);  // 预留100个元素的空间
    // 后续插入不超过100个元素时,不会触发扩容
    

  3. 插入后重新获取迭代器
    若无法提前预留容量,插入后通过索引或 begin() 重新计算迭代器:

    vec.insert(vec.begin() + 1, 99);
    auto it = vec.begin() + 1;  // 重新获取,有效

http://www.xdnf.cn/news/19007.html

相关文章:

  • 旧衣物回收小程序功能模块设计分析
  • 华为无线AC主备配置案例
  • CMake构建学习笔记22-libxml2库的构建
  • 不止于价格,DigitalOcean、AWS和Linode该选谁?
  • Vue3+TS+Element-Plus+el-tree创建树节点
  • 【2025 完美解决】Failed connect to github.com:443; Connection timed out
  • Charles打开后,Pc电脑端浏览器显示Not implemented或没有网络
  • 【计算机组成原理·信息】2数据①
  • 在 Go 项目的 DDD 分层架构中,Echo Web 框架及其 middleware 应该归属到哪一层?
  • LeetCode第二题知识点3 ----引用类型
  • 【C语言强化训练16天】--从基础到进阶的蜕变之旅:Day15
  • OpenCV的轮廓检测
  • 神经语言学与脑科学启发的NLP深层分析:从统计拟合到机制理解的范式转变
  • 基于Spring Boot的短信平台平滑切换设计方案
  • 基于Matlab实现模糊综合评价程序
  • 使用 Java 替换和修改 PDF 文本的方法
  • c++标准模板库
  • 赋能你的应用:英超实时数据接入终极指南(API vs. WebSocket)
  • mongoDB学习(docker)
  • Bert学习笔记
  • HDFS 基本原理与操作流程
  • Python 【深度解析】线程与进程:操作系统中多任务的核心机制
  • 嵌入式第四十一天(数据库)
  • undefined和null
  • 【大模型14】Fine-tuning与大模型优化1
  • HunyuanVideo-Foley视频音效生成模型介绍与部署
  • 【完整源码+数据集+部署教程】胚胎发育阶段检测系统源码和数据集:改进yolo11-SCConv
  • Git 8 ,git 分支开发( 切换分支开发,并设置远程仓库默认分支 )
  • 机器视觉opencv教程(二):二值化、自适应二值化
  • 云计算学习笔记——逻辑卷管理、进程管理、用户提权RAID篇