容器

0 阅读18分钟

什么是容器?

C++ 容器是 C++ 标准模板库(STL)提供的、封装好的通用数据结构模板类,它以类型安全的方式存储和管理一组同类型的元素,通过 RAII 机制自动管理元素的内存生命周期,是现代 C++ 开发中替代原生数组、手动实现数据结构的标准方案。

序列式容器

序列式容器是按元素的「插入顺序」存储和组织的容器,元素的位置由插入时机和位置决定,不会自动按元素值排序,是最基础、最常用的容器类别。

1. std::vector(动态数组)

连续内存的动态数组:在堆上分配一块连续的内存空间,存储元素,当空间不足时自动扩容(通常是 1.5 倍或 2 倍增长)。

核心特点
  • 随机访问 O (1) :通过下标 []at() 直接访问任意位置元素,速度极快。
  • 尾部增删 O (1)push_back()/pop_back() 在尾部操作,平均复杂度 O (1)(扩容时除外)。
  • 中间 / 头部增删 O (n) :在中间或头部插入 / 删除元素,需要移动后面的所有元素,性能较差。
  • 缓存友好:连续内存设计让 CPU 缓存命中率极高,遍历性能远超其他容器。
  • RAII 自动管理:离开作用域自动析构,释放所有元素内存,哪怕抛异常也不泄漏。
关键接口
接口作用注意事项
push_back(val)尾部插入元素会调用拷贝构造,推荐用 emplace_back
emplace_back(args...)尾部直接构造元素零拷贝,直接在容器内存中构造,性能更高
pop_back()尾部删除元素不返回被删元素,要取先 back()
reserve(n)预分配至少 n 个元素的空间提前知道数据量时用,避免频繁扩容
resize(n, val)调整容器大小为 n扩容时用 val 填充新元素,缩容时析构多余元素
[] / at()随机访问元素[] 不做边界检查(越界未定义),at() 越界抛异常
适用场景:
  • 90% 的通用存储场景:不知道用什么容器时,默认选 vector
  • 需要随机访问:通过下标快速访问元素。
  • 仅尾部频繁增删:不在中间 / 头部频繁操作。
  • 替代原生数组:彻底告别原生数组的越界、手动管理内存问题。

2. std::deque(双端队列)

分段连续内存(中控器 + 缓冲区) :不是一整块连续内存,而是由多块 “缓冲区” 组成,通过一个 “中控器”(指针数组)管理这些缓冲区。

核心特点
  • 头尾增删 O (1)push_front()/push_back()/pop_front()/pop_back() 都极快,不需要移动元素。
  • 随机访问 O (1) :支持下标访问,但略慢于 vector(需要通过中控器计算位置)。
  • 中间增删 O (n) :和 vector 一样,中间插入 / 删除需要移动元素。
  • 缓存不如 vector 友好:分段内存设计导致 CPU 缓存命中率略低。
适用场景
  • 需要头尾双向频繁增删:比如滑动窗口、任务队列、消息队列。
  • 需要随机访问,但头部也需要增删vector 不支持 push_front(),此时用 deque
  • 容器适配器的底层默认std::stackstd::queue 的默认底层容器都是 deque

3. std::list(双向循环链表)

双向循环链表:每个元素是一个节点,包含数据、前驱指针、后继指针,节点之间通过指针连接,内存不连续。

核心特点
  • 任意位置增删 O (1) :在任意位置插入 / 删除元素,只需要修改前后节点的指针,不需要移动元素。
  • 不支持随机访问:没有下标 [],只能通过迭代器顺序遍历(从前往后或从后往前)。
  • 遍历性能差:内存不连续,CPU 缓存命中率极低,遍历速度远慢于 vector
  • 内存开销大:每个节点额外存储两个指针(前驱 + 后继),内存占用比 vector 高。
关键接口
接口作用优势
push_front()/push_back()头尾插入O(1)
insert(pos, val)在迭代器 pos 前插入O (1)(只需修改指针)
erase(pos)删除迭代器 pos 处的元素O (1)(只需修改指针)
splice(pos, other)将 other 链表的所有元素移动到 pos 前O (1) 零成本,不拷贝元素
适用场景
  • 频繁在容器中间插入 / 删除元素:比如编辑器的文本插入、链表排序。
  • 不需要随机访问:仅通过迭代器顺序遍历。
  • 需要高效的链表拼接splice() 可以零成本拼接两个链表。

4. std::forward_list(单向链表,C++11 引入)

单向链表:每个节点只有数据和后继指针,没有前驱指针,只能单向遍历。

核心特点
  • list 更省内存:少存一个前驱指针,内存开销更小。
  • 只能单向遍历:没有反向迭代器,不能从后往前遍历。
  • 不支持 push_back()/pop_back() :只能在头部或指定位置操作。
  • 使用场景极少:现代 C++ 开发中几乎不用,除非是极致内存优化的场景。

5. std::array(固定大小数组,C++11 引入)

封装的原生固定数组:在栈上(或对象内部)分配固定大小的连续内存,大小在编译期确定,不能改变。

核心特点
  • 类型安全:替代原生数组,不会隐式退化为指针,支持 size()/empty() 等接口。
  • 性能和原生数组一样:没有任何额外开销,编译期确定大小。
  • 大小固定:不能扩容或缩容,编译器必须知道大小。
适用场景
  • 大小固定且已知:比如存储一周 7 天、一年 12 个月。
  • 替代原生固定数组:既保持原生数组的性能,又有类型安全和统一接口。

有序关联容器

有序关联容器是基于「红黑树(Red-Black Tree)」实现的、自动按 key 排序的关联式容器,它以 “键值对” 或 “单一键” 的形式存储元素,所有元素会按 key 的大小自动升序排列,查找、插入、删除的时间复杂度稳定为 O(logn)

1. std::map(有序键值对字典,最常用)

std::map存储「键值对(std::pair<const Key, Value>)」的有序容器,key 必须唯一,所有元素按 key 自动升序排列。

核心特点
  • key 唯一:同一个 key 只能存在一个元素,插入重复 key 会失败(或覆盖,取决于插入方式)。
  • 自动按 key 排序:默认用 std::less<Key> 比较,也可以自定义比较函数。
  • 稳定 O (logn) 复杂度:查找、插入、删除都是 O (logn)。
  • 支持范围查找:可以高效查找 key 在某个区间内的所有元素。
  • 平均查找速度略慢于 std::unordered_map:红黑树的 O (logn) 比哈希表的平均 O (1) 略慢。
关键接口
接口作用注意事项
insert({key, val})插入键值对key 已存在时插入失败,返回 pair<iterator, bool>
emplace(key, val)直接构造键值对插入零拷贝,性能更高
at(key)访问 key 对应的 valuekey 不存在时抛 std::out_of_range 异常,更安全
find(key)查找 key 对应的迭代器找到返回迭代器,没找到返回 end()
count(key)统计 key 出现的次数对于 map 只能是 0 或 1
erase(key) / erase(iter)删除元素删除 key 或迭代器指向的元素,O (logn)
lower_bound(key)返回第一个 ≥ key 的迭代器范围查找的核心接口
upper_bound(key)返回第一个 > key 的迭代器范围查找的核心接口
equal_range(key)返回 pair<lower_bound, upper_bound>一次性获取 key 对应的范围
适用场景
  • 需要有序的键值对映射:比如按用户 ID 升序存储用户信息、按时间顺序存储日志。
  • 需要范围查找:比如查找 “ID 在 100 到 200 之间的所有用户”。
  • 需要按 key 顺序遍历:比如按分数从低到高输出排行榜。
  • 需要稳定的 O (logn) 性能:不能接受哈希表最坏情况 O (n) 的场景。

2. std::set(有序唯一键集合)

std::set存储「单一键(Key)」的有序容器,key 必须唯一,所有元素按 key 自动升序排列,本质上是 std::map 的 “简化版”(只有 key,没有 value)。

核心特点
  • key 唯一:同一个 key 只能存在一个元素。
  • 自动按 key 排序:默认升序,可自定义比较函数。
  • 稳定 O (logn) 复杂度:查找、插入、删除都是 O (logn)。
  • 支持范围查找:可以高效查找某个区间内的所有 key。
  • 同时完成去重 + 排序:插入元素时自动去重,且自动排序。
关键接口

std::map 类似,但没有 operator[]/at(),因为只有 key 没有 value:

接口作用
insert(key) / emplace(key)插入 key
find(key)查找 key
count(key)统计 key 出现次数(0 或 1)
erase(key) / erase(iter)删除 key
lower_bound(key) / upper_bound(key)范围查找
适用场景
  • 需要同时去重 + 排序:比如存储有序的不重复整数集合、去重后的有序单词列表。
  • 需要快速判断元素是否存在:比如黑名单、白名单,且需要按顺序遍历。
  • 需要范围查找:比如查找 “分数在 80 到 90 之间的所有学生”。

3. std::multimap(有序可重复键值对字典)

std::multimapstd::map 的 “可重复版本”,允许 key 重复,其他特性和 std::map 完全一致(自动排序、O (logn) 复杂度、范围查找)。

核心特点
  • 允许 key 重复:同一个 key 可以对应多个 value。
  • 自动按 key 排序:相同 key 的元素会按插入顺序排列在一起。
  • 稳定 O (logn) 复杂度
  • 支持范围查找:可以通过 equal_range(key) 一次性获取某个 key 对应的所有 value。
关键接口

std::map 类似,但没有 operator[]/at() (因为 key 重复,无法确定返回哪个 value),主要用 equal_range(key) 获取重复 key 的范围。

适用场景
  • 一对多的有序映射:比如按 “班级” 存储多个学生(同一个班级对应多个学生)、按 “日期” 存储多条日志。
  • 需要有序的重复键值对:比如按 “分数” 存储多个学生(允许同分),且按分数排序。

4. std::multiset(有序可重复键集合)

std::multisetstd::set 的 “可重复版本”,允许 key 重复,其他特性和 std::set 完全一致(自动排序、O (logn) 复杂度、范围查找)。

核心特点
  • 允许 key 重复:同一个 key 可以出现多次。
  • 自动按 key 排序:相同 key 的元素会按插入顺序排列在一起。
  • 稳定 O (logn) 复杂度
  • 支持范围查找:可以通过 equal_range(key) 一次性获取某个 key 对应的所有元素。
适用场景
  • 需要有序的重复元素集合:比如存储有序的分数列表(允许同分)、有序的单词频率统计。
  • 需要同时排序 + 允许重复:比如按时间顺序存储所有事件(允许同一时间发生多个事件)。

无序关联容器

无序关联容器是 C++11 引入的、基于「哈希表(Hash Table)」实现的关联式容器,它以 “键值对” 或 “单一键” 的形式存储元素,元素无序,查找、插入、删除的平均时间复杂度为 O (1) ,是现代 C++ 中快速查找、去重场景的首选。

1. std::unordered_map(无序键值对字典,最常用)

std::unordered_map存储「键值对(std::pair<const Key, Value>)」的无序容器,key 必须唯一,通过哈希函数快速定位元素,平均 O (1) 性能。

核心特点
  • key 唯一:同一个 key 只能存在一个元素。
  • 平均 O (1) 极速查找 / 插入 / 删除:性能远快于 std::map
  • 元素无序:不按 key 排序,遍历顺序不确定。
  • 不支持范围查找:无法高效查找 key 在某个区间内的元素。
  • 最坏情况 O (n) :哈希冲突严重时性能退化。
关键接口

std::map 高度一致,但没有范围查找接口:

接口作用注意事项
insert({key, val})插入键值对key 已存在时插入失败,返回 pair<iterator, bool>
emplace(key, val)直接构造键值对插入零拷贝,性能更高
at(key)访问 key 对应的 valuekey 不存在时抛 std::out_of_range 异常,更安全
find(key)查找 key 对应的迭代器找到返回迭代器,没找到返回 end()
count(key)统计 key 出现的次数只能是 0 或 1
erase(key) / erase(iter)删除元素O (1) 平均复杂度
reserve(n)预分配至少 n 个元素的空间提前知道数据量时用,避免频繁重新哈希
bucket_count()返回当前桶的数量用于调试哈希表状态
load_factor()返回当前负载因子用于监控哈希表拥挤程度
适用场景
  • 绝大多数字典 / 映射场景:不需要排序,仅需要快速查找、插入、删除,比如缓存系统、用户 ID 到信息的映射、统计计数。
  • 高性能去重 + 映射:比如统计单词出现频率、IP 地址访问次数。
  • 替代 std::map 的首选:只要不需要有序和范围查找,优先用 unordered_map,性能更高。

2. std::unordered_set(无序唯一键集合)

std::unordered_set存储「单一键(Key)」的无序容器,key 必须唯一,通过哈希函数快速定位元素,平均 O (1) 性能,本质上是 std::unordered_map 的 “简化版”(只有 key,没有 value)。

核心特点
  • key 唯一:同一个 key 只能存在一个元素。
  • 平均 O (1) 极速查找 / 插入 / 删除:性能远快于 std::set
  • 元素无序:不按 key 排序。
  • 不支持范围查找
关键接口

std::unordered_map 类似,但没有 operator[]/at()

接口作用
insert(key) / emplace(key)插入 key
find(key)查找 key
count(key)统计 key 出现次数(0 或 1)
erase(key) / erase(iter)删除 key
reserve(n)预分配空间
适用场景
  • 高性能去重:比如存储不重复的用户 ID、IP 地址、单词列表。
  • 快速判断元素是否存在:比如黑名单、白名单、访问记录统计,不需要排序。
  • 替代 std::set 的首选:只要不需要有序和范围查找,优先用 unordered_set

3. std::unordered_multimap(无序可重复键值对字典)

std::unordered_multimapstd::unordered_map 的 “可重复版本”,允许 key 重复,其他特性完全一致(平均 O (1) 性能、无序、不支持范围查找)。

核心特点
  • 允许 key 重复:同一个 key 可以对应多个 value。
  • 平均 O (1) 性能
  • 元素无序
  • 不支持范围查找
  • 没有 operator[]/at() :因为 key 重复,无法确定返回哪个 value。
关键接口

主要用 equal_range(key) 获取重复 key 的范围。

适用场景
  • 一对多的无序映射:比如按 “班级” 存储多个学生、按 “日期” 存储多条日志,不需要排序。
  • 需要高性能的重复键值对:比如按 “关键词” 存储多个搜索结果,不需要排序。

4. std::unordered_multiset(无序可重复键集合)

std::unordered_multisetstd::unordered_set 的 “可重复版本”,允许 key 重复,其他特性完全一致(平均 O (1) 性能、无序)。

核心特点
  • 允许 key 重复
  • 平均 O (1) 性能
  • 元素无序
适用场景
  • 高性能的重复元素集合:比如存储访问日志的 IP 地址(允许重复)、统计单词出现频率(允许重复),不需要排序。

容器适配器

容器适配器是基于「适配器设计模式」,对现有序列式容器进行二次封装,屏蔽通用接口,仅暴露特定场景专用受限接口的模板类。 它本身不是独立的容器,不直接管理内存、不实现数据存储逻辑,所有核心操作都委托给底层的序列式容器,相当于给通用容器套了一层 “功能外壳”,把灵活的通用容器,转换成语义绝对明确、接口高度受限的专用数据结构。

1. std::stack(栈适配器)

std::stack 是实现 后进先出(LIFO, Last In First Out) 语义的容器适配器,所有操作都只能在 栈顶(尾部) 进行,无法访问栈内的其他元素。

底层实现
  • 默认底层容器std::deque(双端队列)
  • 可选底层容器std::vector / std::list(只要支持 back()push_back()pop_back() 三个接口即可)
核心接口
接口作用时间复杂度
push(val) / emplace(args...)元素入栈(压入栈顶)O (1)(底层容器决定)
pop()栈顶元素出栈(无返回值)O(1)
top()获取栈顶元素的引用O(1)
empty()判断栈是否为空O(1)
size()获取栈内元素个数O(1)
适用场景
  • 表达式求值(如四则运算、括号匹配);
  • 函数调用栈、递归转非递归实现;
  • 深度优先遍历(DFS);
  • 逆序输出、撤销 / 重做操作。

2. std::queue(队列适配器)

std::queue 是实现先进先出(FIFO, First In First Out)语义的容器适配器,元素只能从队尾(尾部)入队,从队头(头部)出队,只能访问队头和队尾元素,无法访问队列中间的元素。

底层实现
  • 默认底层容器std::deque(双端队列)
  • 可选底层容器std::list(必须同时支持 front()/back()/push_back()/pop_front() 四个接口,vector 不支持 pop_front(),因此无法使用)
核心接口
接口作用时间复杂度
push(val) / emplace(args...)元素入队(加入队尾)O(1)
pop()队头元素出队(无返回值)O(1)
front()获取队头元素的引用O(1)
back()获取队尾元素的引用O(1)
empty()判断队列是否为空O(1)
size()获取队列元素个数O(1)
适用场景
  • 任务调度、消息队列、生产者消费者模型;
  • 广度优先遍历(BFS);
  • 流量控制、缓冲区管理;
  • 按顺序处理的事件队列。

3. std::priority_queue(优先队列适配器)

std::priority_queue 是实现优先级出队语义的容器适配器,每次出队的永远是优先级最高的元素,而非先进先出。底层基于堆(Heap)数据结构实现,默认是大顶堆(值越大优先级越高)。

底层实现
  • 默认底层容器std::vector
  • 可选底层容器std::deque(必须支持随机访问迭代器、push_back()pop_back()list 不支持随机访问,因此无法使用)
  • 核心逻辑:在底层容器上自动调用 std::make_heap/std::push_heap/std::pop_heap 堆算法,始终保持堆序性质。
核心接口
接口作用时间复杂度
push(val) / emplace(args...)元素入队,自动调整堆结构O(logn)
pop()优先级最高的元素出队(堆顶),无返回值O(logn)
top()获取堆顶(优先级最高)元素的引用O(1)
empty()判断队列是否为空O(1)
size()获取队列元素个数O(1)
适用场景
  • 任务优先级调度(如操作系统进程调度、事件优先级处理);
  • TopK 问题(如找出数组中前 K 大 / 前 K 小的元素);
  • 堆排序、哈夫曼编码、最短路径算法(Dijkstra);
  • 定时器实现(按超时时间排序)。

函数对象

函数对象是重载了 operator() 的类 / 结构体,它可以像普通函数一样被调用,但比普通函数更强大 —— 可以保存状态、可以作为模板参数、可以内联优化。 std::lessstd::hash 都是 C++ 标准库预定义的模板函数对象,分别服务于有序关联容器无序关联容器

std::less:有序关联容器的默认比较器

std::less 是定义在 <functional> 头文件中的模板函数对象,用于比较两个值的大小,返回「小于」关系的布尔值。 它是所有有序关联容器(map/set/multimap/multiset)的默认比较函数,决定了容器中元素的排序规则。

核心作用:决定有序容器的排序

std::less<int> 会用 int< 运算符比较 key,让容器中的元素按 key 升序排列

如果默认的 std::less 不满足需求(比如想按降序排列、想按自定义类型的某个成员排序),可以自定义比较函数对象,替换默认的 std::less

std::hash:无序关联容器的默认哈希函数

std::hash 是定义在 <functional> 头文件中的模板函数对象,用于将一个值(key)映射成一个整数(哈希值) 。 它是所有无序关联容器(unordered_map/unordered_set/unordered_multimap/unordered_multiset)的默认哈希函数,决定了元素在哈希表桶中的存储位置。

标准库默认支持的类型

C++ 标准库只为内置类型和部分标准库类型提供了默认的 std::hash 特化:

  • 所有基本数据类型:int/long/float/double/bool/char 等;
  • 指针类型;
  • 部分标准库类型:std::string/std::string_view/std::unique_ptr/std::shared_ptr 等。

注意:标准库没有为自定义类型提供默认的 std::hash 特化,必须自己实现。

核心作用:决定无序容器的元素位置

std::hash<int> 负责将 key 映射成哈希值; std::equal_to<int> 负责在哈希冲突时判断两个 key 是否相等(默认用 == 运算符)。

如果想用自定义类型作为 unordered_map/unordered_set 的 key,必须做两件事: 特化 std::hash:为类型提供哈希函数; 提供相等比较:要么重载 operator==,要么自定义 KeyEqual 函数对象。