C++ STL容器使用指南

本文档详细介绍了 C++ STL 容器的分类、使用方法、底层实现原理、性能对比及最佳实践。特别说明各容器在内存管理、迭代器失效和异常安全方面的细节,帮助读者更深入地理解和应用 STL 提供的各个容器。

目录

序列式容器

vector-动态数组

核心特点:连续内存、动态扩容、高效随机访问、尾部操作高效

动态数组(vector)是最常用的容器。它的内存是连续分配的,因此支持快速随机访问;但当容量不足时,会按照一定的扩展策略(通常是倍增扩容)进行内存重新分配,这可能导致所有迭代器失效。

详细说明

  • 内存管理:内存扩容通常遵循几何级增长(如1.5倍或2倍),确保插入元素的均摊时间复杂度为 O(1)
  • 迭代器稳定性:迭代器和指针在重新分配时全部失效,需要注意使用返回的迭代器或重新获取索引
  • 异常安全:提供如at()的边界检查函数,在访问越界时抛出std::out_of_range异常
  • 性能特点:随机访问O(1),尾部插入均摊O(1),中间或头部插入O(n)

💡 小贴士:适用于需要快速随机访问和尾部操作的场景。例如,存储一组数据,并需要频繁访问其中的元素。

使用场景

  • 需要频繁随机访问元素:vector 提供了高效的随机访问能力,可以通过索引直接访问元素
  • 主要在尾部进行插入和删除操作:在尾部插入和删除元素的时间复杂度为 O(1),效率很高
  • 需要动态改变容器大小:vector 可以根据需要动态调整大小,方便存储不确定数量的元素
  • 需要与C风格数组互操作:通过data()方法可获取底层连续内存指针

常用成员函数及使用说明

函数 描述 示例 复杂度
push_back(const T& value) 将元素添加到容器末尾 vec.push_back(5);

// 在末尾添加数值5
均摊O(1)
pop_back() 移除末尾元素(不返回值) vec.pop_back();

// 删除最后一个元素
O(1)
size() 返回容器中的元素数量 size_t len = vec.size();

// 获取元素个数
O(1)
empty() 检查容器是否为空 if(vec.empty()) {...}

// 判断是否为空
O(1)
at(size_t pos) 访问指定位置元素(带边界检查) int val = vec.at(3);

// 访问索引3的元素,越界会抛出异常
O(1)
operator[] 访问指定位置元素(无边界检查) int val = vec[0];

// 访问首个元素,越界行为未定义
O(1)
clear() 清空所有元素 vec.clear();

// 移除所有元素,容量不变
O(n)
insert(iterator pos, const T& value) 在指定位置插入元素 auto it = vec.insert(vec.begin()+2, 10);

// 在索引2的位置插入值10
O(n)
erase(iterator pos) 删除指定位置的元素 auto it = vec.erase(vec.begin()+1);

// 删除索引1位置的元素
O(n)
emplace_back(Args&&... args) 在末尾原位构造元素 vec.emplace_back(42);

// 直接构造值为42的元素
均摊O(1)
reserve(size_t capacity) 预留容量空间但不创建元素 vec.reserve(100);

// 预留100个元素的空间,减少重新分配次数
最坏O(n)
resize(size_t count) 调整容器大小 vec.resize(5);

// 调整大小为5个元素,多余的会被移除,不足的用默认值填充
最坏O(n)
capacity() 返回当前分配的容量 size_t cap = vec.capacity();

// 获取容器当前容量
O(1)
front() 返回首元素的引用 int first = vec.front();

// 获取第一个元素值
O(1)
back() 返回末元素的引用 int last = vec.back();

// 获取最后一个元素值
O(1)
data() 返回指向数据的指针 int* ptr = vec.data();

// 获取底层数组指针,方便与C函数交互
O(1)
shrink_to_fit() 减少内存到适合大小 vec.shrink_to_fit();

// 释放未使用的内存空间
O(n)
assign(size_t count, const T& value) 替换容器内容 vec.assign(3, 0);

// 用3个0替换当前内容
O(n)
swap(vector& other) 与另一容器交换内容 vec1.swap(vec2);

// 交换两个vector的内容
O(1)

构造和初始化示例

// 多种构造方式
std::vector<int> vec1;                    // 默认构造,空容器
std::vector<int> vec2(5, 10);             // 构造包含5个值为10的元素
std::vector<int> vec3 = {1, 2, 3, 4, 5};  // 使用初始化列表
std::vector<int> vec4(vec3.begin(), vec3.end());  // 使用迭代器范围构造

添加删除元素示例

// 添加元素
vec1.push_back(15);          // 在末尾添加元素15
vec1.emplace_back(20);       // 在末尾直接构造元素20(通常比push_back高效)

// 在指定位置添加元素
auto it = vec1.begin() + 1;  // 指向第二个元素的迭代器
vec1.insert(it, 25);         // 在第二个位置插入25
vec1.emplace(it, 30);        // 在第二个位置原位构造30

// 删除元素
vec1.pop_back();             // 移除最后一个元素
vec1.erase(vec1.begin());    // 删除第一个元素
vec1.erase(vec1.begin(), vec1.begin() + 2);  // 删除前两个元素

// 清空容器
vec1.clear();                // 移除所有元素

访问元素示例

// 多种元素访问方式
int first = vec1.front();    // 获取第一个元素
int last = vec1.back();      // 获取最后一个元素
int third = vec1[2];         // 直接访问索引2的元素(不检查边界)
int fourth = vec1.at(3);     // 访问索引3的元素(带边界检查)

// 使用迭代器遍历
for (auto it = vec1.begin(); it != vec1.end(); ++it) {
    std::cout << *it << " ";
}
// 使用范围for循环(C++11起)
for (const auto& item : vec1) {
    std::cout << item << " ";
}

容量操作示例

// 容量管理
vec1.reserve(100);            // 预分配存储空间,减少重新分配
size_t current_capacity = vec1.capacity();  // 获取当前容量
vec1.resize(50);             // 调整大小为50个元素
vec1.resize(75, -1);         // 增大到75个元素,新元素用-1填充
vec1.shrink_to_fit();        // 释放多余的内存

其他常用操作

// 排序和反转
std::sort(vec1.begin(), vec1.end());              // 升序排序整个vector
std::sort(vec1.begin(), vec1.end(), std::greater<int>()); // 降序排序
std::reverse(vec1.begin(), vec1.end());           // 反转元素顺序

// 查找元素
auto it = std::find(vec1.begin(), vec1.end(), 42);
if (it != vec1.end()) {
    std::cout << "找到元素42,位置:" << std::distance(vec1.begin(), it) << std::endl;
}

// 拼接操作
std::vector<int> vec4 = {6, 7, 8};
vec1.insert(vec1.end(), vec4.begin(), vec4.end());  // 将vec4的所有元素追加到vec1末尾

// 使用data()与C风格函数交互
void legacy_function(int* array, size_t size) { /*...*/ }
legacy_function(vec1.data(), vec1.size());

vector迭代器失效情况总结

  1. 添加元素时:

    • 如果导致内存重新分配,则所有迭代器和引用都会失效
    • 如果没有导致重新分配,则插入点之前的迭代器有效,之后的失效
  2. 删除元素时:

    • 被删除位置之前的迭代器有效
    • 被删除位置及之后的迭代器全部失效
// 处理迭代器失效的正确方法
auto it = vec.begin() + 3;
it = vec.insert(it, 42);  // insert返回指向新插入元素的迭代器
it = vec.erase(it);       // erase返回指向被删除元素之后的迭代器

deque-双端队列

核心特点:分段连续内存、两端高效插入删除、随机访问

双端队列采用分段连续内存存储,每段固定大小,这使得两端的插入及删除操作非常高效。

详细说明

  • 内存管理:deque 采用分段连续内存存储,支持在两端 O(1) 时间复杂度的插入和删除操作
  • 迭代器稳定性:分段存储使得 deque 可以在两端进行常数时间的操作,缺点是在中间插入时需要移动块指针,可能丢失部分迭代器
  • 异常安全:与 vector 不同,deque 不支持 reserve 操作,原因在于其非连续内存结构
  • 性能特点:两端插入删除O(1),中间插入删除O(n),随机访问O(1)

💡 注意:deque 不支持 reserve 操作,请参照示例使用。

使用场景

  • 需要在两端频繁插入和删除元素:deque 的两端操作都是 O(1) 复杂度
  • 需要随机访问元素:支持下标访问,与 vector 类似
  • 对中间元素的插入删除操作相对较少:中间操作效率低于两端操作
  • 不希望因容量改变导致迭代器全部失效:相较于 vector,deque 在容量变化时只有部分迭代器失效

常用成员函数及使用说明

函数 描述 示例 复杂度
push_front(const T& value) 在容器前端添加元素 deq.push_front(10);

// 在开始位置插入值10
O(1)
push_back(const T& value) 在容器末尾添加元素 deq.push_back(20);

// 在末尾添加值20
O(1)
pop_front() 移除前端元素 deq.pop_front();

// 删除第一个元素
O(1)
pop_back() 移除末尾元素 deq.pop_back();

// 删除最后一个元素
O(1)
size() 获取元素个数 size_t len = deq.size();

// 获取元素数量
O(1)
empty() 检查是否为空 if(deq.empty()) {...}

// 判断容器是否为空
O(1)
at(size_t pos) 访问指定位置元素(带边界检查) int val = deq.at(2);

// 安全访问索引2的元素
O(1)
operator[size_t pos] 访问指定位置元素(不检查边界) int val = deq[0];

// 直接访问首个元素
O(1)
clear() 清空所有元素 deq.clear();

// 移除所有元素
O(n)
insert(iterator pos, const T& value) 在指定位置插入元素 auto it = deq.insert(deq.begin()+1, 15);

// 在第二个位置插入15
O(n)
erase(iterator pos) 删除指定位置的元素 auto it = deq.erase(deq.begin());

// 删除首个元素
O(n)
emplace_front(Args&&... args) 在前端原位构造元素 deq.emplace_front(5);

// 在开头直接构造元素5
O(1)
emplace_back(Args&&... args) 在末尾原位构造元素 deq.emplace_back(25);

// 在末尾直接构造元素25
O(1)
resize(size_t count) 调整容器大小 deq.resize(10);

// 调整大小为10个元素
最坏O(n)
front() 返回首元素的引用 int first = deq.front();

// 获取第一个元素
O(1)
back() 返回末元素的引用 int last = deq.back();

// 获取最后一个元素
O(1)
shrink_to_fit() 减少内存到适合大小 deq.shrink_to_fit();

// 释放未使用的内存(C++11起)
O(n)
assign(size_t count, const T& value) 替换容器内容 deq.assign(5, 1);

// 用5个值为1的元素替换内容
O(n)
swap(deque& other) 与另一容器交换内容 deq1.swap(deq2);

// 交换两个deque的内容
O(1)

构造和初始化示例

// 多种构造方式
std::deque<int> deq1;                     // 默认构造,空队列
std::deque<int> deq2(5, 10);              // 5个值为10的元素
std::deque<int> deq3 = {1, 2, 3, 4, 5};   // 使用初始化列表
std::deque<int> deq4(deq3.begin(), deq3.begin() + 3);  // 使用迭代器范围构造

两端添加删除示例

// 前端操作
deq1.push_front(50);          // 在前端添加元素50
deq1.emplace_front(40);       // 在前端原位构造元素40
deq1.pop_front();             // 移除前端元素

// 后端操作
deq1.push_back(60);           // 在末尾添加元素60
deq1.emplace_back(70);        // 在末尾原位构造元素70
deq1.pop_back();              // 移除末尾元素

中间插入删除示例

// 中间位置操作
auto mid_it = deq1.begin() + deq1.size() / 2;  // 中间位置迭代器
deq1.insert(mid_it, 30);      // 在中间插入元素30
deq1.erase(mid_it);           // 删除中间位置的元素

访问元素示例

// 访问元素
int first = deq1.front();     // 获取第一个元素
int last = deq1.back();       // 获取最后一个元素
int third = deq1[2];          // 通过索引访问(不检查边界)
int fourth = deq1.at(3);      // 通过at访问(会检查边界)

// 遍历元素
for (auto it = deq1.begin(); it != deq1.end(); ++it) {
    std::cout << *it << " ";
}
// 使用范围for循环
for (const auto& elem : deq1) {
    std::cout << elem << " ";
}

容量管理示例

// 容量相关操作
size_t sz = deq1.size();      // 获取元素数量
bool is_empty = deq1.empty(); // 检查是否为空
deq1.resize(10);              // 调整大小为10个元素
deq1.resize(15, -1);          // 调整为15个元素,新元素用-1填充
deq1.shrink_to_fit();         // 减少内存占用(C++11起)

其他常用操作

// 排序和反转
std::sort(deq1.begin(), deq1.end());  // 升序排序
std::reverse(deq1.begin(), deq1.end()); // 反转元素顺序

// 查找操作
auto find_it = std::find(deq1.begin(), deq1.end(), 30);
if (find_it != deq1.end()) {
    std::cout << "找到元素30" << std::endl;
}

// 拼接操作
std::deque<int> deq5 = {100, 200, 300};
deq1.insert(deq1.end(), deq5.begin(), deq5.end());  // 将deq5拼接到deq1末尾

deque与vector的区别

  • 内存分配:deque 使用分段连续内存,vector 使用单一连续内存
  • 扩容策略:deque 不需要像 vector 那样整体重新分配
  • 两端操作:deque 支持高效的两端操作,vector 只支持高效的尾部操作
  • 迭代器失效:deque 的迭代器在插入删除时部分失效,vector 可能全部失效
// 何时选择deque而非vector?
// 1. 需要频繁在两端添加删除元素
// 2. 不希望因容量变化导致全部迭代器失效
// 3. 元素数量较大且内存分配问题影响性能

list-双向链表

核心特点:节点分散存储、双向链接、高效中间插入删除

双向链表采用节点连续的内存链接,每个节点储存前后指针,允许在任意位置 O(1) 插入或删除,但不支持索引访问。

详细说明

  • 内存管理:由于节点分散存储,list 在迭代时的局部性不如 vector,但在频繁中间修改时非常高效
  • 迭代器稳定性:即使插入或删除操作不会使其他迭代器失效,操作前仍需定位目标位置,时间复杂度为 O(n)
  • 异常安全:list 的插入和删除操作不会抛出异常,除非内存分配失败
  • 性能特点:中间插入删除O(1),随机访问O(n)

💡 优点:高效的中间插入删除;缺点:不支持随机访问。

使用场景

  • 需要频繁在任意位置插入和删除元素:list 的插入删除不会导致其他迭代器失效
  • 不需要随机访问元素:list 不支持下标访问,只能顺序遍历
  • 需要频繁的元素重排:list 的splice操作可以高效地合并、分割链表
  • 元素需要在容器中保持稳定位置:元素的位置不会因为插入删除而改变

常用成员函数及使用说明

函数 描述 示例 复杂度
push_front(const T& value) 在链表开头添加元素 lst.push_front(10);

// 在开头插入值10
O(1)
push_back(const T& value) 在链表末尾添加元素 lst.push_back(20);

// 在末尾插入值20
O(1)
pop_front() 移除链表开头元素 lst.pop_front();

// 删除第一个元素
O(1)
pop_back() 移除链表末尾元素 lst.pop_back();

// 删除最后一个元素
O(1)
size() 获取元素个数 size_t len = lst.size();

// 获取链表长度
O(1)
empty() 检查是否为空 if(lst.empty()) {...}

// 判断链表是否为空
O(1)
clear() 清空所有元素 lst.clear();

// 移除所有元素
O(n)
insert(iterator pos, const T& value) 在指定位置插入元素 auto it = lst.insert(pos, 30);

// 在pos位置插入值30
O(1)
erase(iterator pos) 删除指定位置的元素 auto next_it = lst.erase(it);

// 删除it指向的元素
O(1)
emplace_front(Args&&... args) 在开头原位构造元素 lst.emplace_front(5);

// 在开头直接构造元素5
O(1)
emplace_back(Args&&... args) 在末尾原位构造元素 lst.emplace_back(25);

// 在末尾直接构造元素25
O(1)
resize(size_t count) 调整链表大小 lst.resize(10);

// 调整大小为10个元素
O(n)
front() 返回首元素的引用 int first = lst.front();

// 获取第一个元素
O(1)
back() 返回末元素的引用 int last = lst.back();

// 获取最后一个元素
O(1)
assign(size_t count, const T& value) 替换链表内容 lst.assign(5, 1);

// 用5个值为1的元素替换内容
O(n)
swap(list& other) 与另一链表交换内容 lst1.swap(lst2);

// 交换两个链表的内容
O(1)
splice(iterator pos, list& other) 将另一链表的所有元素移动到此链表 lst1.splice(lst1.begin(), lst2);

// 将lst2的所有元素移到lst1开头
O(1)
merge(list& other) 合并两个有序链表 lst1.merge(lst2);

// 将有序链表lst2合并入lst1
O(n)
unique() 移除相邻重复元素 lst.unique();

// 删除连续重复的元素
O(n)
sort() 对链表进行排序 lst.sort();

// 按升序排序链表
O(n log n)
reverse() 反转链表 lst.reverse();

// 翻转链表元素顺序
O(n)

构造和初始化示例

// 多种构造方式
std::list<int> lst1;                      // 默认构造,空链表
std::list<int> lst2(5, 10);               // 5个值为10的元素
std::list<int> lst3 = {1, 2, 3, 4, 5};    // 使用初始化列表
std::list<int> lst4(lst3.begin(), std::next(lst3.begin(), 3)); // 使用迭代器范围构造

两端添加删除示例

// 前端操作
lst1.push_front(50);           // 在开头添加元素50
lst1.emplace_front(40);        // 在开头原位构造元素40
lst1.pop_front();              // 移除开头元素

// 后端操作
lst1.push_back(60);            // 在末尾添加元素60
lst1.emplace_back(70);         // 在末尾原位构造元素70
lst1.pop_back();               // 移除末尾元素

中间插入删除示例

// 中间位置操作
auto mid_it = std::next(lst1.begin(), lst1.size() / 2);  // 中间位置迭代器
lst1.insert(mid_it, 30);      // 在中间插入元素30
lst1.erase(mid_it);           // 删除中间位置的元素

访问元素示例

// 访问元素
int first = lst1.front();     // 获取第一个元素
int last = lst1.back();       // 获取最后一个元素
int second = *(++lst1.begin()); // 访问第二个元素

// 遍历元素
for (auto it = lst1.begin(); it != lst1.end(); ++it) {
    std::cout << *it << " ";
}
// 使用范围for循环
for (const auto& elem : lst1) {
    std::cout << elem << " ";
}

容量管理示例

// 容量相关操作
size_t sz = lst1.size();      // 获取元素数量
bool is_empty = lst1.empty(); // 检查是否为空
lst1.resize(10);              // 调整大小为10个元素
lst1.resize(15, -1);          // 调整为15个元素,新元素用-1填充

其他常用操作

// 排序和反转
lst1.sort();                  // 升序排序
lst1.reverse();               // 反转元素顺序

// 查找操作
auto find_it = std::find(lst1.begin(), lst1.end(), 30);
if (find_it != lst1.end()) {
    std::cout << "找到元素30" << std::endl;
}

// 拼接操作
std::list<int> lst5 = {100, 200, 300};
lst1.splice(lst1.end(), lst5);  // 将lst5拼接到lst1末尾

list与vector/deque的区别

  • 内存分配:list 使用节点分散存储,vector 和 deque 使用连续内存
  • 访问方式:list 只支持顺序访问,vector 和 deque 支持随机访问
  • 插入删除:list 在任意位置插入删除都是 O(1),vector 和 deque 在中间位置插入删除是 O(n)
  • 迭代器失效:list 的迭代器在插入删除时不会失效,vector 和 deque 可能失效
// 何时选择list而非vector/deque?
// 1. 需要频繁在中间位置插入删除元素
// 2. 不需要随机访问元素
// 3. 需要稳定的迭代器

set-有序集合

核心特点:自动排序、唯一元素、基于红黑树

有序集合基于平衡二叉树(通常为红黑树)实现,所有操作均具有 O(log n) 的时间复杂度。

详细说明

  • 内存管理:元素在插入时自动排好序,内存占用相对较大
  • 迭代器稳定性:迭代器稳定性较好,删除一个元素仅使该元素的迭代器失效
  • 异常安全:所有操作均具有强异常安全性
  • 性能特点:插入删除查找O(log n)

💡 自动排序,适合存储唯一且有序的元素。

使用场景

  • 需要维护有序的唯一元素集合:set 自动排序且不允许重复元素
  • 需要快速查找元素:set 的查找操作是 O(log n) 复杂度
  • 需要范围查询功能:set 提供 lower_bound 和 upper_bound 等接口

常用成员函数及使用说明

函数 描述 示例 复杂度
insert(const T& value) 插入元素 s.insert(10);

// 插入值10
O(log n)
erase(const T& value) 删除指定元素 s.erase(10);

// 删除值10
O(log n)
find(const T& value) 查找元素 auto it = s.find(10);

// 查找值10
O(log n)
count(const T& value) 计算元素出现次数 size_t cnt = s.count(10);

// 计算值10的出现次数
O(log n)
size() 获取元素个数 size_t len = s.size();

// 获取元素数量
O(1)
empty() 检查是否为空 if(s.empty()) {...}

// 判断容器是否为空
O(1)
clear() 清空所有元素 s.clear();

// 移除所有元素
O(n)
emplace(Args&&... args) 原位插入元素 s.emplace(15);

// 直接构造值为15的元素
O(log n)
lower_bound(const T& key) 返回第一个不小于key的迭代器 auto it = s.lower_bound(10);

// 查找第一个不小于10的元素
O(log n)
upper_bound(const T& key) 返回第一个大于key的迭代器 auto it = s.upper_bound(10);

// 查找第一个大于10的元素
O(log n)
equal_range(const T& key) 返回等于key的范围 auto range = s.equal_range(10);

// 查找值10的范围
O(log n)
swap(set& other) 与另一容器交换内容 s1.swap(s2);

// 交换两个set的内容
O(1)

构造和初始化示例

// 多种构造方式
std::set<int> s1;                      // 默认构造,空集合
std::set<int> s2 = {1, 2, 3, 4, 5};    // 使用初始化列表
std::set<int> s3(s2.begin(), s2.end()); // 使用迭代器范围构造

添加删除元素示例

// 添加元素
s1.insert(10);             // 插入值10
s1.emplace(15);            // 直接构造值为15的元素

// 删除元素
s1.erase(10);              // 删除值10
s1.erase(s1.begin());      // 删除第一个元素

查找元素示例

// 查找元素
auto it = s1.find(15);     // 查找值15
if (it != s1.end()) {
    std::cout << "找到元素15" << std::endl;
}

// 计算元素出现次数
size_t cnt = s1.count(15); // 计算值15的出现次数

// 范围查询
auto lower = s1.lower_bound(10); // 查找第一个不小于10的元素
auto upper = s1.upper_bound(10); // 查找第一个大于10的元素
auto range = s1.equal_range(10); // 查找值10的范围

访问元素示例

// 访问元素
for (auto it = s1.begin(); it != s1.end(); ++it) {
    std::cout << *it << " ";
}
// 使用范围for循环
for (const auto& elem : s1) {
    std::cout << elem << " ";
}

其他常用操作

// 清空集合
s1.clear();                // 移除所有元素

// 交换集合
s1.swap(s2);               // 交换s1和s2的内容

set与unordered_set的区别

  • 内存分配:set 使用红黑树,unordered_set 使用哈希表
  • 访问方式:set 自动排序,unordered_set 无序
  • 查找效率:set 的查找是 O(log n),unordered_set 是 O(1)
  • 迭代器失效:set 的迭代器在插入删除时不会失效,unordered_set 可能失效
// 何时选择set而非unordered_set?
// 1. 需要有序存储元素
// 2. 需要范围查询功能
// 3. 需要稳定的迭代器

multiset-有序多重集合

允许存储重复的元素,并按照升序自动排序。 使用场景:需要记录重复值且希望元素自动排序时。

std::multiset<int> ms;
ms.insert(10);
ms.insert(10); // 重复元素允许

map-有序键值对

核心特点:键值对、自动排序、基于红黑树

有序映射和 set 类似,底层也使用红黑树。

详细说明

  • 内存管理:当使用 operator[] 进行元素访问时,如果键不存在,则会自动插入默认值,可能引起额外的内存分配和树重平衡
  • 迭代器稳定性:提供 lower_bound、upper_bound 等接口,便于高效的范围查询
  • 异常安全:所有操作均具有强异常安全性
  • 性能特点:插入删除查找O(log n)

💡 用于建立键值对应关系,并保持键的排序。

使用场景

  • 需要建立键到值的映射关系:map 提供键值对存储
  • 需要按键的顺序访问元素:map 自动按键排序
  • 需要高效的键值查找:map 的查找操作是 O(log n) 复杂度

常用成员函数及使用说明

函数 描述 示例 复杂度
insert(const std::pair<const Key, T>& value) 插入键值对 m.insert({3, "three"});

// 插入键值对(3, "three")
O(log n)
erase(const Key& key) 删除指定键的元素 m.erase(3);

// 删除键为3的元素
O(log n)
find(const Key& key) 查找键对应的元素 auto it = m.find(3);

// 查找键为3的元素
O(log n)
count(const Key& key) 计算键出现次数 size_t cnt = m.count(3);

// 计算键为3的出现次数
O(log n)
size() 获取元素个数 size_t len = m.size();

// 获取元素数量
O(1)
empty() 检查是否为空 if(m.empty()) {...}

// 判断容器是否为空
O(1)
clear() 清空所有元素 m.clear();

// 移除所有元素
O(n)
operator[](const Key& key) 访问或插入键对应的值 m[5] = "five";

// 访问或插入键为5的值
O(log n)
emplace(Args&&... args) 原位插入键值对 m.emplace(4, "four");

// 直接构造键值对(4, "four")
O(log n)
lower_bound(const Key& key) 返回第一个不小于key的迭代器 auto it = m.lower_bound(3);

// 查找第一个不小于3的元素
O(log n)
upper_bound(const Key& key) 返回第一个大于key的迭代器 auto it = m.upper_bound(3);

// 查找第一个大于3的元素
O(log n)
equal_range(const Key& key) 返回等于key的范围 auto range = m.equal_range(3);

// 查找键为3的范围
O(log n)
swap(map& other) 与另一容器交换内容 m1.swap(m2);

// 交换两个map的内容
O(1)

构造和初始化示例

// 多种构造方式
std::map<int, std::string> m1;                      // 默认构造,空映射
std::map<int, std::string> m2 = { {1, "one"}, {2, "two"} }; // 使用初始化列表
std::map<int, std::string> m3(m2.begin(), m2.end()); // 使用迭代器范围构造

添加删除元素示例

// 添加元素
m1.insert({3, "three"});     // 插入键值对(3, "three")
m1.emplace(4, "four");       // 直接构造键值对(4, "four")
m1[5] = "five";              // 访问或插入键为5的值

// 删除元素
m1.erase(3);                 // 删除键为3的元素
m1.erase(m1.begin());        // 删除第一个元素

查找元素示例

// 查找元素
auto it = m1.find(4);        // 查找键为4的元素
if (it != m1.end()) {
    std::cout << "找到键4对应的值: " << it->second << std::endl;
}

// 计算键出现次数
size_t cnt = m1.count(4);    // 计算键为4的出现次数

// 范围查询
auto lower = m1.lower_bound(3); // 查找第一个不小于3的元素
auto upper = m1.upper_bound(3); // 查找第一个大于3的元素
auto range = m1.equal_range(3); // 查找键为3的范围

访问元素示例

// 访问元素
for (auto it = m1.begin(); it != m1.end(); ++it) {
    std::cout << it->first << ": " << it->second << " ";
}
// 使用范围for循环
for (const auto& elem : m1) {
    std::cout << elem.first << ": " << elem.second << " ";
}

其他常用操作

// 清空映射
m1.clear();                  // 移除所有元素

// 交换映射
m1.swap(m2);                 // 交换m1和m2的内容

map与unordered_map的区别

  • 内存分配:map 使用红黑树,unordered_map 使用哈希表
  • 访问方式:map 自动排序,unordered_map 无序
  • 查找效率:map 的查找是 O(log n),unordered_map 是 O(1)
  • 迭代器失效:map 的迭代器在插入删除时不会失效,unordered_map 可能失效
// 何时选择map而非unordered_map?
// 1. 需要有序存储键值对
// 2. 需要范围查询功能
// 3. 需要稳定的迭代器

multimap-有序多重映射

允许相同键对应多个值,自动按键的升序排序。 使用场景:需要建立一个键映射多个值时。

std::multimap<int, std::string> mm;
mm.insert({1, "one"});
mm.insert({1, "uno"}); // 同一键的重复插入

无序关联式容器

unordered_set-哈希集合

核心特点:基于哈希表、无序存储、快速查找

基于哈希表实现,所有查找、插入操作在平均情况下为 O(1)。

详细说明

  • 内存管理:元素无序存储,内存占用相对较小
  • 迭代器稳定性:内部采用开放地址或链地址法解决冲突,不同实现对内存和性能的要求不同
  • 异常安全:当负载因子过高时,rehash 操作会导致所有元素重新分布,迭代器也可能失效
  • 性能特点:插入删除查找平均O(1)

💡 优点:查找速度快,无须保持顺序。

使用场景

  • 只需要判断元素是否存在:unordered_set 提供高效的查找操作
  • 不需要元素保持有序:unordered_set 无序存储
  • 追求最快的元素查找速度:unordered_set 的查找操作是 O(1) 复杂度

常用成员函数及使用说明

函数 描述 示例 复杂度
insert(const T& value) 插入元素 us.insert(10);

// 插入值10
平均O(1)
erase(const T& value) 删除指定元素 us.erase(10);

// 删除值10
平均O(1)
find(const T& value) 查找元素 auto it = us.find(10);

// 查找值10
平均O(1)
count(const T& value) 计算元素出现次数 size_t cnt = us.count(10);

// 计算值10的出现次数
平均O(1)
size() 获取元素个数 size_t len = us.size();

// 获取元素数量
O(1)
empty() 检查是否为空 if(us.empty()) {...}

// 判断容器是否为空
O(1)
clear() 清空所有元素 us.clear();

// 移除所有元素
O(n)
emplace(Args&&... args) 原位插入元素 us.emplace(15);

// 直接构造值为15的元素
平均O(1)
bucket_count() 返回当前哈希桶的数量 size_t n = us.bucket_count();

// 获取哈希桶数量
O(1)
load_factor() 返回当前负载因子 float lf = us.load_factor();

// 获取负载因子
O(1)
rehash(size_t n) 重新配置哈希桶数量 us.rehash(20);

// 重新配置哈希桶数量
O(n)
swap(unordered_set& other) 与另一容器交换内容 us1.swap(us2);

// 交换两个unordered_set的内容
O(1)

构造和初始化示例

// 多种构造方式
std::unordered_set<int> us1;                      // 默认构造,空集合
std::unordered_set<int> us2 = {1, 2, 3, 4, 5};    // 使用初始化列表
std::unordered_set<int> us3(us2.begin(), us2.end()); // 使用迭代器范围构造

添加删除元素示例

// 添加元素
us1.insert(10);             // 插入值10
us1.emplace(15);            // 直接构造值为15的元素

// 删除元素
us1.erase(10);              // 删除值10
us1.erase(us1.begin());     // 删除第一个元素

查找元素示例

// 查找元素
auto it = us1.find(15);     // 查找值15
if (it != us1.end()) {
    std::cout << "找到元素15" << std::endl;
}

// 计算元素出现次数
size_t cnt = us1.count(15); // 计算值15的出现次数

访问元素示例

// 访问元素
for (auto it = us1.begin(); it != us1.end(); ++it) {
    std::cout << *it << " ";
}
// 使用范围for循环
for (const auto& elem : us1) {
    std::cout << elem << " ";
}

其他常用操作

// 清空集合
us1.clear();                // 移除所有元素

// 交换集合
us1.swap(us2);              // 交换us1和us2的内容

// 获取哈希桶数量
size_t n = us1.bucket_count(); // 获取哈希桶数量

// 获取负载因子
float lf = us1.load_factor();  // 获取负载因子

// 重新配置哈希桶数量
us1.rehash(20);             // 重新配置哈希桶数量

unordered_set与set的区别

  • 内存分配:unordered_set 使用哈希表,set 使用红黑树
  • 访问方式:unordered_set 无序,set 自动排序
  • 查找效率:unordered_set 的查找是 O(1),set 是 O(log n)
  • 迭代器失效:unordered_set 的迭代器在插入删除时可能失效,set 不会失效
// 何时选择unordered_set而非set?
// 1. 只需要判断元素是否存在
// 2. 不需要元素保持有序
// 3. 追求最快的查找速度

unordered_map-哈希表

核心特点:基于哈希表、键值对、快速查找

详细说明与 unordered_set 类似,另外键值对的存储允许快速的键值映射。

详细说明

  • 内存管理:注意在使用 operator[] 时,如果键不存在会创建新项,可能带来意外的内存分配
  • 迭代器稳定性:内部采用开放地址或链地址法解决冲突,不同实现对内存和性能的要求不同
  • 异常安全:当负载因子过高时,rehash 操作会导致所有元素重新分布,迭代器也可能失效
  • 性能特点:插入删除查找平均O(1)

💡 常用于快速键值查找和数据插入。

使用场景

  • 需要快速的键值查找:unordered_map 提供高效的查找操作
  • 不需要键值对保持有序:unordered_map 无序存储
  • 存储大量数据时需要高效查找:unordered_map 的查找操作是 O(1) 复杂度

常用成员函数及使用说明

函数 描述 示例 复杂度
insert(const std::pair<const Key, T>& value) 插入键值对 um.insert({3, "three"});

// 插入键值对(3, "three")
平均O(1)
erase(const Key& key) 删除指定键的元素 um.erase(3);

// 删除键为3的元素
平均O(1)
find(const Key& key) 查找键对应的元素 auto it = um.find(3);

// 查找键为3的元素
平均O(1)
count(const Key& key) 计算键出现次数 size_t cnt = um.count(3); <br// 计算键为3的出现次数 平均O(1)
size() 获取元素个数 size_t len = um.size(); <br// 获取元素数量 O(1)
empty() 检查是否为空 if(um.empty()) {...} <br// 判断容器是否为空 O(1)
clear() 清空所有元素 um.clear(); <br// 移除所有元素 O(n)
operator[](const Key& key) 访问或插入键对应的值 um[5] = "five"; <br// 访问或插入键为5的值 平均O(1)
emplace(Args&&... args) 原位插入键值对 um.emplace(4, "four"); <br// 直接构造键值对(4, "four") 平均O(1)
bucket_count() 返回当前哈希桶的数量 size_t n = um.bucket_count(); <br// 获取哈希桶数量 O(1)
load_factor() 返回当前负载因子 float lf = um.load_factor(); <br// 获取负载因子 O(1)
rehash(size_t n) 重新配置哈希桶数量 um.rehash(20); <br// 重新配置哈希桶数量 O(n)
swap(unordered_map& other) 与另一容器交换内容 um1.swap(um2); <br// 交换两个unordered_map的内容 O(1)

构造和初始化示例

// 多种构造方式
std::unordered_map<int, std::string> um1;                      // 默认构造,空映射
std::unordered_map<int, std::string> um2 = { {1, "one"}, {2, "two"} }; // 使用初始化列表
std::unordered_map<int, std::string> um3(um2.begin(), um2.end()); // 使用迭代器范围构造

添加删除元素示例

// 添加元素
um1.insert({3, "three"});     // 插入键值对(3, "three")
um1.emplace(4, "four");       // 直接构造键值对(4, "four")
um1[5] = "five";              // 访问或插入键为5的值

// 删除元素
um1.erase(3);                 // 删除键为3的元素
um1.erase(um1.begin());       // 删除第一个元素

查找元素示例

// 查找元素
auto it = um1.find(4);        // 查找键为4的元素
if (it != um1.end()) {
    std::cout << "找到键4对应的值: " << it->second << std::endl;
}

// 计算键出现次数
size_t cnt = um1.count(4);    // 计算键为4的出现次数

访问元素示例

// 访问元素
for (auto it = um1.begin(); it != um1.end(); ++it) {
    std::cout << it->first << ": " << it->second << " ";
}
// 使用范围for循环
for (const auto& elem : um1) {
    std::cout << elem.first << ": " << elem.second << " ";
}

其他常用操作

// 清空映射
um1.clear();                  // 移除所有元素

// 交换映射
um1.swap(um2);                // 交换um1和um2的内容

// 获取哈希桶数量
size_t n = um1.bucket_count(); // 获取哈希桶数量

// 获取负载因子
float lf = um1.load_factor();  // 获取负载因子

// 重新配置哈希桶数量
um1.rehash(20);               // 重新配置哈希桶数量

unordered_map与map的区别

  • 内存分配:unordered_map 使用哈希表,map 使用红黑树
  • 访问方式:unordered_map 无序,map 自动排序
  • 查找效率:unordered_map 的查找是 O(1),map 是 O(log n)
  • 迭代器失效:unordered_map 的迭代器在插入删除时可能失效,map 不会失效
// 何时选择unordered_map而非map?
// 1. 需要快速的键值查找
// 2. 不需要键值对保持有序
// 3. 存储大量数据时需要高效查找

容器适配器

queue-队列

核心特点:FIFO结构、基于deque实现、只允许访问队首和队尾

队列是一种 FIFO 数据结构,内部一般使用 deque 实现。

详细说明

  • 内存管理:由于只允许访问队首和队尾,不支持迭代器,因此无法直接对队列内部数据做复杂操作
  • 迭代器稳定性:在多线程环境中,队列必须进行额外的同步来保证数据的一致性
  • 异常安全:所有操作均具有强异常安全性
  • 性能特点:插入删除O(1),访问队首队尾O(1)

💡 先进先出(FIFO)结构,常用于任务调度及广度优先搜索。

使用场景

  • 需要按照先进先出的顺序处理元素:queue 提供 FIFO 结构
  • 实现广度优先搜索:queue 常用于 BFS 算法
  • 需要按照时间顺序处理任务:queue 适合任务调度

常用成员函数及使用说明

函数 描述 示例 复杂度
push(const T& value) 添加元素到队列末尾 q.push(10); <br// 在末尾添加值10 O(1)
pop() 移除队列前端元素 q.pop(); <br// 删除第一个元素 O(1)
front() 访问队列前端元素 int front = q.front(); <br// 获取第一个元素 O(1)
back() 访问队列末尾元素 int back = q.back(); <br// 获取最后一个元素 O(1)
empty() 检查是否为空 if(q.empty()) {...} <br// 判断容器是否为空 O(1)
size() 获取元素个数 size_t len = q.size(); <br// 获取元素数量 O(1)
emplace(Args&&... args) 原位添加元素到队列末尾 q.emplace(15); <br// 直接构造值为15的元素 O(1)
swap(queue& other) 与另一容器交换内容 q1.swap(q2); <br// 交换两个queue的内容 O(1)

构造和初始化示例

// 多种构造方式
std::queue<int> q1;                      // 默认构造,空队列
std::queue<int> q2;                      // 使用默认构造
q2.push(1);                              // 添加元素
q2.push(2);                              // 添加元素

添加删除元素示例

// 添加元素
q1.push(10);             // 在末尾添加值10
q1.emplace(15);          // 直接构造值为15的元素

// 删除元素
q1.pop();                // 删除第一个元素

访问元素示例

// 访问元素
int front = q1.front();  // 获取第一个元素
int back = q1.back();    // 获取最后一个元素

容量管理示例

// 容量相关操作
size_t sz = q1.size();   // 获取元素数量
bool is_empty = q1.empty(); // 检查是否为空

其他常用操作

// 交换队列
q1.swap(q2);             // 交换q1和q2的内容

queue与deque的区别

  • 内存分配:queue 使用 deque 作为底层容器
  • 访问方式:queue 只允许访问队首和队尾,deque 支持随机访问
  • 插入删除:queue 只允许在队首和队尾插入删除,deque 支持在任意位置插入删除
  • 迭代器失效:queue 不支持迭代器,deque 支持迭代器
// 何时选择queue而非deque?
// 1. 需要先进先出的顺序处理元素
// 2. 不需要随机访问元素
// 3. 需要简单的接口

priority_queue-优先队列

核心特点:基于堆、默认最大堆、支持自定义比较器

优先队列基于堆实现,默认是最大堆。

详细说明

  • 内存管理:内部维护堆结构,使得每次获取 top 操作都是 O(1),而插入和删除操作都是 O(log n)
  • 迭代器稳定性:可通过自定义比较器改变排序规则,例如实现最小堆
  • 异常安全:所有操作均具有强异常安全性
  • 性能特点:插入删除O(log n),访问top O(1)

💡 默认实现最大堆,可根据需定制排序规则。

使用场景

  • 需要维护一个动态的有序序列:priority_queue 提供堆结构
  • 实现堆排序:priority_queue 常用于堆排序算法
  • 需要频繁获取最大/最小元素:priority_queue 提供高效的 top 操作

常用成员函数及使用说明

函数 描述 示例 复杂度
push(const T& value) 添加元素 pq.push(10); <br// 添加值10 O(log n)
pop() 移除优先级最高的元素 pq.pop(); <br// 删除优先级最高的元素 O(log n)
top() 访问优先级最高的元素 int top = pq.top(); <br// 获取优先级最高的元素 O(1)
empty() 检查是否为空 if(pq.empty()) {...} <br// 判断容器是否为空 O(1)
size() 获取元素个数 size_t len = pq.size(); <br// 获取元素数量 O(1)
emplace(Args&&... args) 原位添加元素 pq.emplace(15); <br// 直接构造值为15的元素 O(log n)
swap(priority_queue& other) 与另一容器交换内容 pq1.swap(pq2); <br// 交换两个priority_queue的内容 O(1)

构造和初始化示例

// 多种构造方式
std::priority_queue<int> pq1;                      // 默认构造,空优先队列
std::priority_queue<int> pq2;                      // 使用默认构造
pq2.push(1);                                       // 添加元素
pq2.push(2);                                       // 添加元素

添加删除元素示例

// 添加元素
pq1.push(10);             // 添加值10
pq1.emplace(15);          // 直接构造值为15的元素

// 删除元素
pq1.pop();                // 删除优先级最高的元素

访问元素示例

// 访问元素
int top = pq1.top();      // 获取优先级最高的元素

容量管理示例

// 容量相关操作
size_t sz = pq1.size();   // 获取元素数量
bool is_empty = pq1.empty(); // 检查是否为空

其他常用操作

// 交换优先队列
pq1.swap(pq2);            // 交换pq1和pq2的内容

priority_queue与queue的区别

  • 内存分配:priority_queue 使用堆结构,queue 使用 deque 作为底层容器
  • 访问方式:priority_queue 只允许访问优先级最高的元素,queue 只允许访问队首和队尾
  • 插入删除:priority_queue 的插入删除是 O(log n),queue 的插入删除是 O(1)
  • 迭代器失效:priority_queue 不支持迭代器,queue 不支持迭代器
// 何时选择priority_queue而非queue?
// 1. 需要维护一个动态的有序序列
// 2. 需要频繁获取最大/最小元素
// 3. 需要实现堆排序

stack-栈

核心特点:LIFO结构、基于deque实现、只允许访问栈顶

栈是一种 LIFO 数据结构,通常使用 deque 实现。

详细说明

  • 内存管理:栈中的所有操作均为常数时间,但由于不支持迭代器,其调试和错误检查较为困难
  • 迭代器稳定性:主要适用于递归调用、表达式求值等场景
  • 异常安全:所有操作均具有强异常安全性
  • 性能特点:插入删除O(1),访问栈顶O(1)

💡 后进先出(LIFO)结构,适合实现深度优先搜索或函数调用栈。

使用场景

  • 需要按照后进先出的顺序处理元素:stack 提供 LIFO 结构
  • 实现深度优先搜索:stack 常用于 DFS 算法
  • 处理括号匹配等问题:stack 适合括号匹配等问题
  • 实现函数调用栈:stack 适合函数调用栈

常用成员函数及使用说明

函数 描述 示例 复杂度
push(const T& value) 添加元素到栈顶 s.push(10); <br// 在栈顶添加值10 O(1)
pop() 移除栈顶元素 s.pop(); <br// 删除栈顶元素 O(1)
top() 访问栈顶元素 int top = s.top(); <br// 获取栈顶元素 O(1)
empty() 检查是否为空 if(s.empty()) {...} <br// 判断容器是否为空 O(1)
size() 获取元素个数 size_t len = s.size(); <br// 获取元素数量 O(1)
emplace(Args&&... args) 原位添加元素到栈顶 s.emplace(15); <br// 直接构造值为15的元素 O(1)
swap(stack& other) 与另一容器交换内容 s1.swap(s2); <br// 交换两个stack的内容 O(1)

构造和初始化示例

// 多种构造方式
std::stack<int> s1;                      // 默认构造,空栈
std::stack<int> s2;                      // 使用默认构造
s2.push(1);                              // 添加元素
s2.push(2);                              // 添加元素

添加删除元素示例

// 添加元素
s1.push(10);             // 在栈顶添加值10
s1.emplace(15);          // 直接构造值为15的元素

// 删除元素
s1.pop();                // 删除栈顶元素

访问元素示例

// 访问元素
int top = s1.top();      // 获取栈顶元素

容量管理示例

// 容量相关操作
size_t sz = s1.size();   // 获取元素数量
bool is_empty = s1.empty(); // 检查是否为空

其他常用操作

// 交换栈
s1.swap(s2);             // 交换s1和s2的内容

stack与deque的区别

  • 内存分配:stack 使用 deque 作为底层容器
  • 访问方式:stack 只允许访问栈顶元素,deque 支持随机访问
  • 插入删除:stack 只允许在栈顶插入删除,deque 支持在任意位置插入删除
  • 迭代器失效:stack 不支持迭代器,deque 支持迭代器
// 何时选择stack而非deque?
// 1. 需要后进先出的顺序处理元素
// 2. 不需要随机访问元素
// 3. 需要简单的接口

位集合

bitset-固定大小位容器

核心特点:固定大小、位操作高效、适合布尔标志存储

用于存储固定数量的二进制位,支持高效位运算。

详细说明

  • 内存管理:因为固定大小,bitset 的操作在编译期间就确定,性能和内存使用非常优化
  • 迭代器稳定性:在处理大批量布尔值数据时,比 vector 更加高效且易于理解
  • 异常安全:所有操作均具有强异常安全性
  • 性能特点:位操作O(1)

💡 用于存储固定数量的二进制位,并支持高效的位操作。

使用场景

  • 需要按位处理或存储大量布尔标志:bitset 提供高效的位操作

常用成员函数及使用说明

函数 描述 示例 复杂度
set() 将所有位或指定位置的位设置为1 bs.set(); <br// 将所有位设置为1 O(1)
reset() 将所有位或指定位置的位重置为0 bs.reset(); <br// 将所有位重置为0 O(1)
flip() 将所有位或指定位置的位取反 bs.flip(); <br// 将所有位取反 O(1)
test(size_t pos) 检查指定位置的位是否为1 bool bit3 = bs.test(3); <br// 检查第3位是否为1 O(1)
all() 检查所有位是否都为1 bool all = bs.all(); <br// 检查所有位是否都为1 O(1)
any() 检查是否存在为1的位 bool any = bs.any(); <br// 检查是否存在为1的位 O(1)
none() 检查是否所有位都为0 bool none = bs.none(); <br// 检查是否所有位都为0 O(1)
count() 返回为1的位的个数 size_t cnt = bs.count(); <br// 返回为1的位的个数 O(1)
size() 返回位的总数 size_t sz = bs.size(); <br// 返回位的总数 O(1)
to_string() 返回位的字符串表示 std::string str = bs.to_string(); <br// 返回位的字符串表示 O(n)
to_ulong() 返回位的无符号长整型表示 unsigned long ul = bs.to_ulong(); <br// 返回位的无符号长整型表示 O(1)
to_ullong() 返回位的无符号长长整型表示 unsigned long long ull = bs.to_ullong(); <br// 返回位的无符号长长整型表示 O(1)

构造和初始化示例

// 多种构造方式
std::bitset<8> bs1;                      // 默认构造,空位集合
std::bitset<8> bs2(0b10101010);          // 使用二进制字面量
std::bitset<8> bs3("1100");              // 使用字符串

设置和重置位示例

// 设置和重置位
bs1.set();                // 将所有位设置为1
bs1.reset();              // 将所有位重置为0
bs1.set(3);               // 将第3位置为1
bs1.reset(3);             // 将第3位重置为0
bs1.flip();               // 将所有位取反
bs1.flip(2);              // 将第2位取反

检查位示例

// 检查位
bool bit3 = bs1.test(3);  // 检查第3位是否为1
bool all = bs1.all();     // 检查所有位是否都为1
bool any = bs1.any();     // 检查是否存在为1的位
bool none = bs1.none();   // 检查是否所有位都为0
size_t cnt = bs1.count(); // 返回为1的位的个数
size_t sz = bs1.size();   // 返回位的总数

转换为字符串和整数示例

// 转换为字符串和整数
std::string str = bs1.to_string(); // 返回位的字符串表示
unsigned long ul = bs1.to_ulong(); // 返回位的无符号长整型表示
unsigned long long ull = bs1.to_ullong(); // 返回位的无符号长长整型表示

示例:bitset默认索引从右到左(低位到高位)

std::bitset<4> bs(0b1010); // 表示:第3位为1,第2位为0,第1位为1,第0位为0
// 例如:测试最高位
bool highest = bs.test(3); // 返回true

性能对比

容器类型 随机访问 插入/删除(开始) 插入/删除(中间) 插入/删除(结尾) 查找
vector O(1) O(n) O(n)* O(1) O(n)
deque O(1) O(1) O(n)* O(1) O(n)
list O(n) O(1) O(n) (遍历定位 + O(1)) O(1) O(n)
set O(log n) O(log n) O(log n) O(log n) O(log n)
map O(log n) O(log n) O(log n) O(log n) O(log n)
unordered_set N/A O(1) O(1) O(1) O(1)
unordered_map N/A O(1) O(1) O(1) O(1)

*注:中间插入/删除需要先遍历定位,list操作定位为O(n)

空间复杂度

容器类型 额外空间
vector O(n)
deque O(n)
list O(n)
set O(n)
map O(n)
unordered_set O(n)
unordered_map O(n)

最佳实践

  1. 默认优先使用 vector
  2. 需要在两端操作时使用 deque
  3. 频繁插入删除使用 list
  4. 需要有序唯一元素使用 set
  5. 需要键值对使用 map
  6. 追求查找效率使用 unordered_set/map
  7. 需要FIFO使用 queue
  8. 需要维护有序序列使用 priority_queue

补充说明

  • 使用 emplace 系列函数可以避免构造临时对象,从而提高性能。
  • 对于可能抛出异常的操作(如 vector::at()、map::at()),建议使用 try-catch 捕获异常。

容器适用场景对比

容器类型 内存占用 查找效率 插入/删除效率 是否有序 允许重复元素
vector 较低 O(n) 尾部 O(1),中间 O(n) 是 否
deque 较低 O(n) 两端 O(1),中间 O(n) 是 否
list 较高 O(n) O(1) 否 是
set 中等 O(log n) O(log n) 是 否
multiset 中等 O(log n) O(log n) 是 是
map 中等 O(log n) O(log n) 是 键唯一
multimap 中等 O(log n) O(log n) 是 键可重复
unordered_set 较低 平均 O(1) 平均 O(1) 否 否
unordered_map 较低 平均 O(1) 平均 O(1) 否 键唯一

迭代器失效说明

在 vector 和 deque 中,插入或删除元素可能导致迭代器失效:

详细说明:

  • 对 vector:插入或删除元素可能导致重新分配内存,从而使所有迭代器失效。建议使用返回值或重新获取迭代器。
  • 对 deque:插入/删除头尾元素通常安全,但中间操作可能使部分迭代器失效。

示例:

// 避免迭代器失效的示例
auto it = vec.begin();
it = vec.insert(it, 42); // 使用返回的迭代器继续操作

std::array 的说明

虽然 std::array 大小固定,但适用于需要在栈上分配小型固定数组的场景,性能较高且安全:

#include <array>
std::array<int, 5> arr = {1, 2, 3, 4, 5};

查找操作详解

详细查找操作部分增加对异常安全性、底层实现(例如哈希桶分布、红黑树平衡规则)的解释,帮助你更好地选择合适的查找算法和数据结构进行优化。

顺序容器查找

vector/deque 的查找

std::vector<int> vec = {1, 2, 3, 4, 5};

// 1. 使用 find 算法(需要包含 <algorithm>)
auto it = std::find(vec.begin(), vec.end(), 3);
if (it != vec.end()) {
    std::cout << "找到元素: " << *it << "\n";
} else {
    std::cout << "未找到元素\n";
}

// 2. 使用 find_if 查找满足条件的元素
auto it2 = std::find_if(vec.begin(), vec.end(), [](int x) { 
    return x > 3; 
});
if (it2 != vec.end()) {
    std::cout << "找到第一个大于3的元素: " << *it2 << "\n";
}

// 3. 二分查找(要求容器已排序)
if (std::binary_search(vec.begin(), vec.end(), 3)) {
    std::cout << "元素3存在\n";
}

// 4. 获取位置
auto idx = std::distance(vec.begin(), it);
std::cout << "元素位置: " << idx << "\n";

list 的查找

std::list<int> lst = {1, 2, 3, 4, 5};

// 由于list不支持随机访问,通常使用迭代器
auto it = std::find(lst.begin(), lst.end(), 3);
if (it != lst.end()) {
    std::cout << "找到元素: " << *it << "\n";
    // 前移/后移迭代器
    ++it; // 下一个元素
    --it; // 上一个元素
}

关联容器查找

set 的查找

std::set<int> s = {1, 2, 3, 4, 5};

// 1. 使用 find 成员函数
auto it = s.find(3);
if (it != s.end()) {
    std::cout << "找到元素: " << *it << "\n";
}

// 2. 使用 count 检查元素是否存在(set中要么是0要么是1)
if (s.count(3) > 0) {
    std::cout << "元素3存在\n";
}

// 3. 使用 lower_bound 和 upper_bound
auto lower = s.lower_bound(3); // 大于等于3的第一个元素
auto upper = s.upper_bound(3); // 大于3的第一个元素

if (lower != s.end()) {
    std::cout << "第一个大于等于3的元素: " << *lower << "\n";
}

// 4. 使用 equal_range 获取范围
auto range = s.equal_range(3);
for (auto it = range.first; it != range.second; ++it) {
    std::cout << *it << " ";
}

map 的查找

std::map<std::string, int> m = {
    {"one", 1}, {"two", 2}, {"three", 3}
};

// 1. 使用 find 成员函数
auto it = m.find("two");
if (it != m.end()) {
    std::cout << "找到键two对应的值: " << it->second << "\n";
}

// 2. 使用 count 检查键是否存在
if (m.count("two") > 0) {
    std::cout << "键two存在\n";
}

// 3. 使用 at 访问(不存在则抛出异常)
try {
    int value = m.at("two");
    std::cout << "键two的值: " << value << "\n";
} catch (const std::out_of_range& e) {
    std::cout << "键不存在\n";
}

// 4. 使用 operator[] (不存在则插入默认值)
int value = m["two"]; // 如果不存在,会插入并初始化为0

// 5. 使用 lower_bound 和 upper_bound(基于键排序)
auto lower = m.lower_bound("two");
if (lower != m.end()) {
    std::cout << "第一个大于等于two的键值对: " 
              << lower->first << ":" << lower->second << "\n";
}

无序容器查找

unordered_set 的查找

std::unordered_set<int> us = {1, 2, 3, 4, 5};

// 1. 使用 find 成员函数
auto it = us.find(3);
if (it != us.end()) {
    std::cout << "找到元素: " << *it << "\n";
}

// 2. 使用 count 检查元素是否存在
if (us.count(3) > 0) {
    std::cout << "元素3存在\n";
}

// 注意:无序容器不支持 lower_bound/upper_bound

unordered_map 的查找

std::unordered_map<std::string, int> um = {
    {"one", 1}, {"two", 2}, {"three", 3}
};

// 1. 使用 find 成员函数
auto it = um.find("two");
if (it != um.end()) {
    std::cout << "找到键two对应的值: " << it->second << "\n";
}

// 2. 使用 count 检查键是否存在
if (um.count("two") > 0) {
    std::cout << "键two存在\n";
}

// 3. 安全的值访问模式
auto it2 = um.find("two");
if (it2 != um.end()) {
    std::cout << "值: " << it2->second << "\n";
} else {
    std::cout << "键不存在\n";
}

// 4. 使用 at(不存在则抛出异常)
try {
    int value = um.at("two");
} catch (const std::out_of_range& e) {
    std::cout << "键不存在\n";
}

容器适配器的查找

注意:stack、queue 和 priority_queue 不支持直接查找,只能访问特定位置的元素(如栈顶、队首等)

std::stack<int> s;
s.push(1);
s.push(2);
s.push(3);

// 只能访问栈顶元素
if (!s.empty()) {
    std::cout << "栈顶元素: " << s.top() << "\n";
}

std::queue<int> q;
q.push(1);
q.push(2);
q.push(3);

// 只能访问队首和队尾元素
if (!q.empty()) {
    std::cout << "队首元素: " << q.front() << "\n";
    std::cout << "队尾元素: " << q.back() << "\n";
}

std::priority_queue<int> pq;
pq.push(1);
pq.push(3);
pq.push(2);

// 只能访问最高优先级的元素(默认最大值)
if (!pq.empty()) {
    std::cout << "最高优先级元素: " << pq.top() << "\n";
}

查找最佳实践

  1. 对于有序序列,优先使用二分查找 (binary_search, lower_bound, upper_bound)
  2. 对于关联容器,优先使用成员函数 find 而不是算法 std::find
  3. 需要频繁查找时,考虑使用哈希容器 (unordered_set/map)
  4. 需要查找范围时,使用 equal_range
  5. 对于 map/unordered_map,优先使用 find 而不是 operator[] 检查键是否存在

扩展说明及用法

1. STL各容器的选择建议

  • 对于需要动态扩展且随机访问要求高的场景,尽可能使用 vector。
  • 当两端插入删除频繁时,优先考虑 deque;而中间频繁插入删除建议使用 list。
  • 对于需要快速查询,且数据无须顺序时,可选择 unordered_set 或 unordered_map。

2. 详细使用示例

示例:使用 vector 与算法配合

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
    vector<int> vec = {5, 3, 8, 1};
    // 排序并查找第一个大于4的元素
    sort(vec.begin(), vec.end());
    auto it = find_if(vec.begin(), vec.end(), [](int n){ return n > 4; });
    if(it != vec.end()){
        cout << "第一个大于4的元素是: " << *it << endl;
    }
    return 0;
}

3. 扩展用法说明

  • 为提高代码复用性,建议将常用操作封装为函数或类模板。
  • 利用 Lambda 表达式进行复杂条件查找,是 C++11 后的常用技巧。
  • STL 算法与容器的灵活组合,能够大幅简化代码且提高性能,应多加实践。

扩展阅读与总结

  • 推荐书籍:

    • 《Effective STL》
    • 《STL源码剖析》
    • 《C++标准库》
  • 在线资源:

  • 学习建议:

    • 在实际项目中尝试不同容器以平衡性能与空间;
    • 注意容器的迭代器失效规则与内存管理细节;
    • 关注 STL 算法使用案例,提升编码规范和效率。

迭代器类别

  • vector / deque:随机访问迭代器(支持 +、-、[] 运算符)
  • list / set / map:双向迭代器(仅支持 ++ 和 --)
  • forward_list:前向迭代器(仅支持 ++)

C++ STL 中的迭代器按照功能强大程度可以分为以下几类:

  1. 随机访问迭代器

    • 适用容器:vector、deque
    • 支持的操作:
    it + n;      // 向前移动n个位置
    it - n;      // 向后移动n个位置
    it[n];       // 访问第n个元素
    it1 < it2;   // 迭代器比较
    
  2. 双向迭代器

    • 适用容器:list、set、map
    • 支持的操作:
    ++it;    // 向前移动一个位置
    --it;    // 向后移动一个位置
    *it;     // 解引用访问元素
    
  3. 前向迭代器

    • 适用容器:forward_list
    • 支持的操作:
    ++it;    // 只能向前移动
    *it;     // 解引用访问元素
    

使用示例

// 随机访问迭代器示例
std::vector<int> vec = {1, 2, 3, 4, 5};
auto it = vec.begin();
it += 2;      // 可以直接跳转
std::cout << it[1] << std::endl;  // 可以用[]访问

// 双向迭代器示例
std::list<int> lst = {1, 2, 3, 4, 5};
auto it2 = lst.begin();
++it2;    // 只能一步一步移动
--it2;    // 可以向后移动

// 前向迭代器示例
std::forward_list<int> fl = {1, 2, 3, 4, 5};
auto it3 = fl.begin();
++it3;    // 只能向前移动