STL 容器
概念讲解
从底层结构"看见"容器
学习容器不能只背 API,而要在脑子里建立每个容器的"长相"。容器的所有特性——快在哪、慢在哪、是否有序、会不会去重——全部由它的底层数据结构决定。看懂了结构,复杂度表不用背也能推出来。
vector:一排连续的格子。 元素一个紧挨一个放在连续内存里。知道起点和下标就能一步算出元素地址,所以 v[i] 是 O(1)。格子装满后它会另找一块约两倍大的新区域、把旧元素整体搬过去(扩容),因此尾部追加是"均摊 O(1)";而在中间插入需要挪动后面的所有元素,是 O(n)。
下标: 0 1 2 3
+----+----+----+----+
| 3 | 1 | 4 | 1 | 内存连续,v[2] 一步跳到
+----+----+----+----+list:用指针串起来的节点。 每个节点除数据外还存前后两个指针,内存不连续。插入删除只需修改指针(O(1)),但访问第 i 个元素必须沿指针从头走(O(n)),竞赛中很少使用,了解即可。
+----+ +----+ +----+
| 3 | -> | 1 | -> | 4 |
+----+ +----+ +----+set / map:一棵始终保持平衡的二叉搜索树(红黑树)。 左子树都比根小、右子树都比根大,按"左-根-右"的中序遍历天然得到升序序列;红黑树会自动保持平衡,树高约为 ,所以增删查都是 O(log n)。
5
/ \
3 7 存放 {1,3,5,7,9}
/ \ 中序遍历:1 3 5 7 9(自动有序)
1 9unordered_set / unordered_map:一张哈希表。 元素通过哈希函数被分配到一个个"桶"中,理想情况下一次就能定位,平均 O(1);代价是元素之间没有任何顺序保证。
桶 0 | -> (k,v) -> (k,v)
桶 1 |
桶 2 | -> (k,v)
...priority_queue:用数组表示的完全二叉树(堆)。 下标从 1 开始时,节点 i 的父亲是 i/2,孩子是 2i 和 2i+1。大根堆保证每个父亲都不小于孩子,所以根(数组开头)永远是最大值,取最值只需 O(1),插入弹出为 O(log n)。
数组下标: 1 2 3 4 5
+----+----+----+----+----+
| 9 | 7 | 5 | 1 | 3 |
+----+----+----+----+----+
9 数组表示的大根堆
/ \
7 5 每个父亲 >= 孩子
/ \ 堆顶 9 即最大值
1 3stack / queue:包装出来的适配器。 它们本身不负责存储数据,而是套在 deque(默认底层容器)外面,只开放"一端进出"或"两端各管一头"的受限接口。
常用容器与复杂度
| 容器 | 底层结构 | [i] 随机访问 |
头部增删 | 尾部增删 | 按值查找 | 顺序与去重 |
|---|---|---|---|---|---|---|
| vector | 动态数组 | O(1) | O(n) | 均摊 O(1) | O(n) | 按插入顺序、可重复 |
| deque | 分段数组 | O(1) | O(1) | O(1) | O(n) | 按插入顺序、可重复 |
| list | 双向链表 | O(n) | O(1) | O(1) | O(n) | 按插入顺序、可重复 |
| set / map | 红黑树 | 不支持 | — | — | O(log n) | 自动按 key 排序、去重 |
| unordered_set / map | 哈希表 | 不支持 | — | — | 平均 O(1),最坏 O(n) | 无序、去重 |
| priority_queue | 堆 | 只能看堆顶 O(1) | — | push/pop 为 O(log n) | — | 每次弹出最大(小)值 |
| stack / queue | 适配器 | 不支持 | — | O(1) | 不支持查找 | LIFO / FIFO |
如何选择容器
- 默认首选 vector:需要按下标访问、只在尾部追加的场景占绝大多数,连续内存对缓存也最友好。
- 需要频繁在头部增删(如 BFS 队列的底层)→ deque。
- 要求元素始终有序、去重,或按值快速查找且数据量不小 → set / map。
- 只关心"在不在"或"键对应什么值",不关心顺序,追求平均 O(1) → unordered_set / unordered_map。
- 每次都要取当前最大(小)值,如 Top K、Dijkstra → priority_queue。
- 先进后出(括号匹配、表达式、DFS)用 stack;先进先出(BFS)用 queue。
vector
vector 是竞赛中使用频率最高的容器,相当于长度可以自动增长的数组。
1234567891011#include <vector>
vector<int> v;
v.push_back(1); // 尾部添加
v.pop_back(); // 尾部删除(不返回被删元素)
v.size(); // 元素个数
v.empty(); // 是否为空
v[i]; // 随机访问,O(1),注意下标不能越界
v.front(); // 第一个元素,等价 v[0]
v.back(); // 最后一个元素,等价 v[v.size()-1]
v.clear(); // 清空全部元素常见的构造方式与二维数组:
123456789101112vector<int> a; // 空数组
vector<int> b(5); // 5 个元素,初值均为 0
vector<int> c(5, -1); // 5 个元素,初值均为 -1
vector<int> d = {3, 1, 4, 1, 5}; // 列表初始化
int n = 4, m = 5;
vector<vector<int>> g(n, vector<int>(m)); // n 行 m 列的二维数组,初值全 0
g[0][0] = 1;
for (int i = 0; i < n; i++) // 按下标遍历
for (int j = 0; j < m; j++) { ... }
for (int x : d) cout << x << ' '; // 范围 for 遍历,只读reserve 与 resize 是初学者最容易混淆的一对:
1234vector<int> v;
v.reserve(100); // 只预留容量(capacity),size 仍是 0,不能访问 v[0]
v.resize(10); // 改变元素个数:多出的元素补 0,此时 v[0..9] 都可访问
v.resize(20, -1); // 新增的 10 个元素填 -1,原有元素不变如果事先知道要放入多少元素,先 reserve(n) 可以避免 push_back 过程中反复扩容、整体搬家的开销。
deque
deque(双端队列)支持在头部和尾部都 O(1) 增删,同时也支持 [i] 随机访问;代价是内存分段连续、额外开销比 vector 略大。
1234567891011#include <deque>
deque<int> dq;
dq.push_back(2); // 尾部加
dq.push_front(1); // 头部加
dq.pop_back(); // 尾部删
dq.pop_front(); // 头部删
dq.front(); // 队首
dq.back(); // 队尾
dq[0]; // 支持随机访问 O(1)
dq.clear();stack 与 queue
它们是容器适配器,默认基于 deque 实现,接口刻意做得很小。
1234567891011121314#include <stack>
#include <queue>
stack<int> st;
st.push(1); // 入栈
st.top(); // 查看栈顶(不弹出)
st.pop(); // 弹出栈顶(pop 不返回元素!)
st.empty(); st.size();
queue<int> q;
q.push(1); // 入队(尾部)
q.front(); // 队首
q.back(); // 队尾
q.pop(); // 队首出队stack 是先进后出(LIFO),用于括号匹配、表达式求值、DFS 的手动栈;queue 是先进先出(FIFO),用于 BFS。两者都不能遍历、不能按下标访问。
set
set 自动完成排序与去重:重复插入同一个值不会产生第二份拷贝,遍历结果总是从小到大。
12345678910111213141516#include <set>
set<int> s;
s.insert(3); // 插入,重复插入无效
s.insert(1);
s.insert(5);
s.erase(1); // 按值删除
s.count(3); // 存在返回 1,不存在返回 0(set 中只有这两种结果)
s.find(3); // 返回指向该元素的迭代器;不存在时返回 s.end()
s.size(); // 大小
s.empty(); s.clear();
for (int x : s) cout << x << ' '; // 输出 3 5,自动升序
auto it = s.lower_bound(3); // 第一个 >= 3 的元素
auto it2 = s.upper_bound(3); // 第一个 > 3 的元素判断元素是否存在,推荐用 s.find(x) != s.end() 或 s.count(x)。lower_bound / upper_bound 是 set 的高频考点,可以在 O(log n) 内找到区间边界。
如果要让自定义类型放进 set,必须为它定义比较规则(红黑树需要靠它排序):
12345678struct Student {
int id;
string name;
bool operator<(const Student& o) const {
return id < o.id; // 按 id 排序、去重
}
};
set<Student> ss;map
map 存储"键 → 值"的映射,键自动排序且不重复。最常用的场景是计数。
12345678910111213#include <map>
map<string, int> mp;
mp["key"] = 1; // 插入/修改
mp.erase("key"); // 删除
mp.count("key"); // 键是否存在
mp.find("key"); // 查找,不存在返回 mp.end()
mp.size(); // 大小
// 遍历(按键升序输出)
for (auto& [k, v] : mp) {
cout << k << ": " << v << endl;
}特别注意 mp[key] 的副作用:键不存在时会自动插入一个默认值(int 为 0)。这正好让计数写法 mp[x]++ 成立(第一次出现时先创建 0 再加 1);但如果只想查询、不想增加元素,应使用 find 或 count。
1234567map<char, int> cnt;
cnt['a']++; // 'a' 不存在:先自动创建为 0,再自增为 1
cnt['a']++; // 已存在:直接自增为 2
if (cnt.count('z')) { ... } // 只判断,不插入
auto it = cnt.find('z'); // 只查找,不插入
if (it != cnt.end()) cout << it->second;unordered_set 与 unordered_map
接口与 set/map 几乎完全相同,区别只在于底层是哈希表:平均 O(1) 查找,但不保证遍历顺序。
12345678910111213#include <unordered_map>
#include <unordered_set>
unordered_map<string, int> cnt;
cnt["apple"]++;
cnt["banana"]++;
for (auto& [k, v] : cnt) {
cout << k << ": " << v << endl; // 输出顺序不确定,不要依赖
}
unordered_set<int> us;
us.insert(3);
if (us.count(3)) { ... } // O(1) 平均选择建议:只需要快速查找且不关心顺序时用 unordered 版本,速度通常更快;需要按序遍历、找前驱后继(lower_bound),或担心哈希冲突被卡时,用 set/map 更稳妥。
list(了解)
双向链表,头尾增删 O(1),但不支持 [i] 随机访问。竞赛中绝大多数场景都被 vector + deque 覆盖,了解即可。
1234567#include <list>
list<int> l;
l.push_front(1);
l.push_back(2);
l.pop_front();
l.pop_back();pair
pair 把两个值捆绑成一个整体,在坐标、边权、多关键字排序中极其常用。
123456789#include <utility> // <bits/stdc++.h> 已包含
pair<int, string> p = {1, "abc"};
cout << p.first << ' ' << p.second; // 1 abc
vector<pair<int, int>> points;
points.push_back({3, 5}); // 也可用 make_pair(3, 5)
sort(points.begin(), points.end()); // 默认先比 first,相同再比 second优先队列
优先队列(堆)每次弹出的都是当前优先级最高的元素。默认是大根堆,堆顶为最大值;需要最小值优先时要手动改成小根堆。
123456789101112#include <queue>
// 大根堆(默认)
priority_queue<int> maxHeap;
// 小根堆:第二个参数指定底层容器,第三个参数指定比较方式
priority_queue<int, vector<int>, greater<int>> minHeap;
maxHeap.push(1); // 插入,O(log n)
maxHeap.top(); // 查看堆顶,O(1)
maxHeap.pop(); // 弹出堆顶,O(log n)
maxHeap.empty(); maxHeap.size();三个模板参数的写法是固定套路,记忆方法是:小根堆就加 vector<int>, greater<int>。
让自定义类型进优先队列,需要提供比较规则——注意比较方向与直觉相反:返回 a 排在 b 后面 为真时,b 优先出堆。
123456789struct Node {
int id, dist;
};
struct Cmp {
bool operator()(const Node& a, const Node& b) const {
return a.dist > b.dist; // dist 小的排前面(小根堆),用于 Dijkstra
}
};
priority_queue<Node, vector<Node>, Cmp> pq;priority_queue<pair<int,int>> 可以直接使用:默认先按 first、再按 second 取大值。
bitset(位数组,入门)
bitset 把一个固定长度的 0/1 序列当作容器操作,可以按下标访问每一位,常用于状态压缩与调试时打印变量的二进制形式。
1234567#include <bitset>
bitset<8> b(42); // 用整数 42 构造:00101010
cout << b << endl; // 直接输出二进制
b.set(0); // 第 0 位置 1
b.flip(); // 全部取反
cout << b.count() << endl; // 其中 1 的个数bitset 的完整用法(打印补码、位运算、突破 64 位状态压缩等)在「位运算进阶」一节有专门讲解。
考点分析
初赛考查方式
- 选择题: 容器特性
- 程序阅读: STL 操作
- 编程题: 容器应用
常见考点
- 根据底层结构推导容器特性与时间复杂度
- 根据题目场景选择合适容器
- set / map 的有序性、去重与 lower_bound
- map 的
[]自动插入语义 - 大根堆 / 小根堆的写法与自定义比较
- 迭代器失效与边遍历边删除
例题精讲
例题 1:set 去重
题目: 对数组去重并排序。
解答:
123set<int> s;
for (int i = 0; i < n; i++) s.insert(a[i]);
for (int x : s) cout << x << " ";例题 2:map 计数
题目: 统计字符串中每个字符出现次数。
解答:
12345map<char, int> mp;
for (char c : s) mp[c]++;
for (auto& [c, cnt] : mp) {
cout << c << ": " << cnt << endl;
}例题 3:小根堆求 Top K
题目: 不断读入数字,最后保留最大的 3 个并按升序输出。
解答: 维护一个大小始终不超过 3 的小根堆:新元素入堆后,若堆超过 3 个就弹掉堆顶(当前最小值)。最后堆里剩下的恰好是全局最大的 3 个数。
123456789101112131415161718#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> a = {5, 1, 9, 3, 7, 2, 8};
priority_queue<int, vector<int>, greater<int>> pq; // 小根堆
for (int x : a) {
pq.push(x);
if ((int)pq.size() > 3) pq.pop(); // 弹走最小的,保留最大的 3 个
}
while (!pq.empty()) { // 依次弹出 7 8 9(升序)
cout << pq.top() << ' ';
pq.pop();
}
return 0;
}练习题
- vector 和 deque 的区别是什么?
查看答案
| 特性 | vector | deque |
|---|---|---|
| 底层结构 | 动态数组 | 双端队列 |
| 随机访问 | O(1) | O(1) |
| 头部操作 | O(n) | O(1) |
| 尾部操作 | O(1) | O(1) |
| 空间 | 连续内存 | 分段连续 |
- set 和 unordered_set 的区别是什么?
查看答案
| 特性 | set | unordered_set |
|---|---|---|
| 底层结构 | 红黑树 | 哈希表 |
| 有序性 | 有序 | 无序 |
| 插入 | O(log n) | O(1) 平均 |
| 查找 | O(log n) | O(1) 平均 |
| 删除 | O(log n) | O(1) 平均 |
- 如何创建小根堆?
查看答案
12345678910#include <queue>
// 小根堆
priority_queue<int, vector<int>, greater<int>> pq;
pq.push(3);
pq.push(1);
pq.push(2);
cout << pq.top(); // 输出 1- 如何求一个数组中第 k 大的数?
查看答案
用大小为 k 的小根堆:遍历数组逐个入堆,堆大小超过 k 就弹出堆顶(最小值),遍历结束后堆顶就是第 k 大的数。时间复杂度 O(n log k),比全部排序 O(n log n) 更优。
12345678int kthLargest(vector<int>& a, int k) {
priority_queue<int, vector<int>, greater<int>> pq;
for (int x : a) {
pq.push(x);
if ((int)pq.size() > k) pq.pop();
}
return pq.top();
}易错点
⚠️ 注意:
- set 自动排序和去重,遍历总是升序;unordered_set 既不排序也不保证遍历顺序。
- map 用
[]访问不存在的 key 会自动插入默认值 0,纯查询要用 find/count。- priority_queue 默认是大根堆,取最小值要写
priority_queue<int, vector<int>, greater<int>>;取 top 前先确认堆非空。- vector 扩容会使原先的迭代器、引用全部失效;reserve 只预留容量,不改变 size,不能访问还没 resize 出来的元素。
- 边遍历 set/map 边删除时,要用
it = s.erase(it)接收返回的下一个迭代器,先erase(it++)或直接++it再删都是常见错误。- stack/queue 的 pop() 不返回元素,取值得先调 top()/front()。
- size() 返回的是无符号整数,与 int 做减法或比较时建议先强转
(int)v.size(),避免下溢成巨大的数。- unordered 容器平均 O(1) 但最坏 O(n),且自定义类型作 key 需额外提供哈希函数;int、string 等内置类型可直接使用。
- 自定义类型放入 set/map/priority_queue 必须定义明确的比较规则,否则编译报错。