C++ STL常用容器介绍
好的我们来系统地梳理一下C STL中那些最常用的容器。我会从“是什么”、“底层怎么实现”、“核心特性与复杂度”以及“最佳适用场景”这几个维度来总结帮你一次性吃透它们。一、顺序容器线性存储讲究顺序这类容器的元素按插入顺序线性排列核心差异在于内存布局和访问/修改的效率。1.std::vector动态数组底层实现一段连续的内存空间。内部维护三个指针或迭代器start数据起始、finish已用空间末尾、end_of_storage总容量末尾。核心特性随机访问O(1)缓存局部性极好遍历速度最快。尾部插入/删除均摊 O(1)。当容量不足时会触发扩容通常申请原容量1.5~2倍的新内存然后将旧元素移动或拷贝过去。中间/头部插入/删除O(n)因为需要移动后续所有元素。迭代器失效扩容时所有迭代器、指针、引用全部失效。中间插入/删除时从操作位置到末尾的迭代器失效。适用场景默认首选。需要频繁随机访问、尾部增删、排序、批量处理的场景。2.std::deque双端队列底层实现分段连续空间。由一个中央控制器map本质是一个指针数组管理多个固定大小的连续内存块buffer。逻辑上连续物理上分段。核心特性双端操作push_front/pop_front和push_back/pop_back均为 O(1)。随机访问O(1)但比vector稍慢需要先定位到段再计算段内偏移。中间插入/删除O(n)。迭代器失效两端插入/删除时迭代器可能失效但引用和指针通常保持有效指向已存在元素。中间操作会导致所有迭代器失效。适用场景需要在两端频繁操作同时还需要随机访问的场景如滑动窗口算法、任务调度队列。3.std::list双向链表底层实现双向链表。每个节点包含数据、前驱指针和后继指针节点在堆上独立分配。核心特性任意位置插入/删除O(1)前提是已知目标位置的迭代器。随机访问O(n)不支持下标操作。额外开销每个节点需要存储两个指针内存开销较大。迭代器失效稳定性最好。插入操作不会使任何已有迭代器失效删除操作仅使被删除节点的迭代器失效。适用场景需要在容器中间位置频繁进行插入和删除操作且不关心随机访问的场景如编辑器的撤销/重做栈、音乐播放列表。4.std::forward_list单向链表底层实现单向链表。每个节点只包含一个后继指针。核心特性比list更节省内存但只能单向遍历没有size()方法获取大小需要遍历。适用场景内存极度受限且仅需单向遍历的场景。5.std::array固定大小数组底层实现封装了C风格原生数组大小在编译期确定。核心特性栈上分配连续内存零额外开销。不支持动态增删。适用场景大小固定、对性能要求极高且无需动态扩容的场景如嵌入式开发、作为函数返回值的固定大小缓冲区。二、关联容器有序存储快速查找这类容器基于键Key来组织元素内部自动排序查找效率稳定。1.std::set/std::multiset底层实现红黑树Red-Black Tree一种自平衡的二叉搜索树。核心特性自动排序元素按Key升序排列可自定义比较器。去重set中键唯一multiset允许重复。所有操作查找、插入、删除的时间复杂度均为O(log n)。迭代器失效插入操作通常不会使其他迭代器失效删除操作仅使被删节点的迭代器失效。适用场景需要元素自动排序且唯一set或需要稳定的 O(log n) 查找性能。2.std::map/std::multimap底层实现红黑树。存储的是pairconst Key, Value键值对。核心特性自动排序按键排序。键唯一性map中键唯一multimap允许重复键。所有操作查找、插入、删除均为O(log n)。适用场景需要构建有序的字典或映射关系且对查找、插入、删除效率有稳定要求如电话簿、配置项管理。三、无序关联容器哈希加速平均最快C11 引入基于哈希表实现不保证元素顺序但平均性能最高。1.std::unordered_set/std::unordered_multiset底层实现哈希表。通常采用“数组 链表”拉链法实现。一个桶数组Bucket Array每个桶指向一个链表当链表过长时某些实现会将其转换为红黑树以优化最坏情况。核心特性无序元素不排序。平均性能查找、插入、删除均为O(1)。最坏情况当哈希冲突严重时可能退化为O(n)。适用场景对元素顺序无要求追求极致查找速度的场景如缓存、去重检查。2.std::unordered_map/std::unordered_multimap底层实现哈希表。存储键值对。核心特性与unordered_set类似平均 O(1) 的查找、插入、删除。适用场景最常用的字典类型。需要快速通过键查找值且不关心元素顺序如统计词频、建立ID到对象的映射。总结与选型决策容器类别容器名称底层数据结构查找复杂度插入/删除复杂度核心特点首选场景顺序vector动态数组O(1) 随机访问尾部O(1)中间O(n)连续内存缓存友好默认首选随机访问尾部操作deque分段连续数组O(1) 随机访问头尾O(1)中间O(n)双端高效双端队列、滑动窗口list双向链表O(n)已知位置O(1)迭代器稳定插入删除快中间频繁增删关联set/map红黑树O(log n)O(log n)自动排序稳定高效需要有序映射/集合无序unordered_*哈希表平均 O(1)平均 O(1)无序查找最快默认字典选择追求速度一句话决策指南要数组随机访问→vector要字符串→string本质是vectorchar要键值对快速查找→unordered_map要键值对且需要有序→map要双端操作→deque要中间频繁插入删除→list要去重→set或unordered_setmap和set的区别这两个容器在 C STL 里关系很近底层都是红黑树但用途和存储结构完全不同。一、存储内容不同最本质的区别容器存储的是什么形象理解set只有键Key一个集合只关心某个元素存不存在map键值对Key-Value一本字典通过键找到对应的值代码对比// set只存学号判断某个学号是否存在setintstudentIds;studentIds.insert(1001);// map存学号→姓名通过学号查姓名mapint,stringstudentMap;studentMap[1001]张三;二、元素访问方式不同容器如何访问值能否用[]下标set只能通过迭代器遍历没有下标操作❌ 不行map通过[]或at()按键访问值✅ 可以map[key]特别注意map的[]运算符有个副作用——如果键不存在它会自动插入一个默认值。所以只做查询时建议用find()或at()避免意外插入。三、修改权限不同容器键/元素能否修改值能否修改set元素不可修改const—map键不可修改const✅值可以修改为什么不能改因为红黑树是靠键来排序的改了键会破坏树的结构。所以 set 的元素和 map 的键都是const的。如果想修改只能先删除旧的再插入新的。四、核心用途不同容器核心用途典型场景set存在性检查 去重 有序遍历记录已访问的URL、统计不重复元素、自动排序map键值映射 快速查找字典单词→释义、学号→成绩、配置项管理五、一句话总结对比维度setmap存储内容只有 KeyKey-Value 对下标访问❌ 不支持✅ 支持[]和at()修改权限元素不可改键不可改值可改核心用途去重、查存在映射、查值底层结构红黑树红黑树时间复杂度O(log n)O(log n)选型口诀只关心有没有 →set关心有什么对应的值 →mapstd::list 的底层核心设计——带头节点的循环双向链表一、核心结构一个“环” 一个“哨兵”std::list的底层可以拆解为三个关键词双向、循环、带头节点。双向每个节点list_node内部包含三个部分_data存储的实际数据。_next指向下一个节点的指针。_prev指向上一个节点的指针。这使得链表可以双向遍历。循环整个链表首尾相连形成一个环。最后一个有效节点的_next指向头节点。头节点的_prev指向最后一个有效节点。带头节点哨兵节点这是最巧妙的设计。链表里有一个特殊的节点称为头节点或哨兵节点它不存储任何有效数据只作为链表的“锚点”。二、为什么需要这个“哨兵节点”这是 STLlist设计的精髓所在它的核心价值是统一逻辑消除边界特判。想象一下如果没有这个哨兵节点即head指针直接指向第一个有效节点或为nullptr那么在进行插入、删除操作时你需要写大量if...else来处理特殊情况空链表插入if (head nullptr) { head newNode; }在头部插入if (pos head) { ... }在尾部插入if (pos nullptr) { ... }删除最后一个节点需要特殊处理head指针。有了哨兵节点后一切变得简单而统一空链表状态哨兵节点的_next和_prev都指向它自己。begin() end()循环自然退出。插入操作在任何位置包括头部和尾部插入的逻辑完全一致都是“找到前驱节点修改四个指针”无需任何特判。删除操作同样删除任何节点包括第一个和最后一个的逻辑也完全一致。三、迭代器与这个“环”的关系list的迭代器本质上是对Node*指针的封装。begin()返回指向哨兵节点的下一个节点即第一个有效节点的迭代器。end()返回指向哨兵节点本身的迭代器。这是一个“过去式”的结束标志它不指向任何有效数据。遍历过程当你从begin()开始不断it迭代器会沿着_next指针前进经过所有有效节点最终当it end()时意味着你绕了一圈又回到了哨兵节点遍历结束。四、一张图看懂假设链表里有 1、2、3 三个元素结构如下┌───────────────────────────────────────────┐ │ ▼ ┌─────────┴─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 哨兵节点 (_head) │ │ 节点 1 │ │ 节点 2 │ │ 节点 3 │ │ (不存数据) │ │ data1 │ │ data2 │ │ data3 │ │ _next ──────────►│──►│ _next │──►│ _next │──►│ _next │──► │ _prev ◄──────────│◄──│ _prev │◄──│ _prev │◄──│ _prev │◄── └───────────────────┘ └──────────┘ └──────────┘ └──────────┘ ▲ └───────────────────────────────────────────┘begin()指向节点1。end()指向哨兵节点。空链表时哨兵节点的_next和_prev都指向自己形成一个只有一个节点的环。五、总结这个设计的三大优势代码简洁插入、删除等所有操作逻辑完全统一无需任何边界条件判断代码量减少错误率降低。迭代器稳定list的迭代器非常稳定。插入操作不会使任何已有迭代器失效删除操作仅使被删除节点的迭代器失效。这在复杂逻辑中极其省心。性能确定任意位置的插入和删除都是纯粹的指针操作时间复杂度恒为O(1)前提是你已经拿到了目标位置的迭代器。理解了这个“带头循环双向链表”的结构你就真正理解了std::list的设计哲学——用空间一个哨兵节点换取逻辑的简洁和性能的稳定。