C++ 竞赛入门

STL 总结

按需求选工具:容器与算法选择表、复杂度速查与选择自测。

STL 总结

读完能做什么

  • 拿到一个任务,能先问“数据要按什么规则存、按什么规则取”,再说出该用哪种容器。
  • 能说出 vector、set、map、queue、priority_queue 处理同一组 [3,1,3,2] 时,各自观察到的结果有什么不同。
  • 能根据“保存重复记录 / 按先来后到处理 / 统计出现次数 / 找第 k 小”选出对应工具。
  • 能说出常见误区:unique 只合相邻、sort 不稳定、map[] 会插入、binary_search 返回布尔值。

为什么需要它

学完前面各页,你手里多了一堆名字:vector、list、set、map、queue、stack、priority_queue、sort、find、unique……真正的难点不再是“某个函数怎么用”,而是遇到一道题,该选哪一个。

这一页不重复讲语法,只做一件事:把“需求”和“工具”对上号,并指出每个选择背后的理由。

它是什么

一句话:这是一张任务 → 工具的对照表,外加几条判断依据。

选择时先回答两个问题:数据要按什么规则保存?按什么规则取出来?

graph TD
    A["拿到一个问题"] --> B{"数据按什么规则取用?"}
    B -->|"按先后顺序一条条处理"| C["queue:先进先出"]
    B -->|"后进的先处理"| D["stack:后进先出"]
    B -->|"每次取最大或最优先"| E["priority_queue"]
    B -->|"按下标随机访问"| F["vector 或 deque"]
    B -->|"按键查找、要不要重复"| G["set 或 map"]

最小可运行示例

同一组数据 [3,1,3,2],放进不同容器,看到的东西完全不同:

#include <bits/stdc++.h>
using namespace std;

int main() {
    vector<int> v = {3, 1, 3, 2};
    cout << "vector 保存原顺序:";
    for (int x : v) cout << x << ' ';
    cout << '\n';

    set<int> s = {3, 1, 3, 2};
    cout << "set 有序去重:";
    for (int x : s) cout << x << ' ';
    cout << '\n';

    map<int, int> cnt;
    for (int x : v) cnt[x]++;
    cout << "map 逐项计数:";
    for (const auto& p : cnt) cout << p.first << ':' << p.second << ' ';
    cout << '\n';

    queue<int> q;
    for (int x : v) q.push(x);
    cout << "queue 先进先出:";
    while (!q.empty()) {
        cout << q.front() << ' ';
        q.pop();
    }
    cout << '\n';

    priority_queue<int> pq;
    for (int x : v) pq.push(x);
    cout << "priority_queue 最大值优先:";
    while (!pq.empty()) {
        cout << pq.top() << ' ';
        pq.pop();
    }
    cout << '\n';
    return 0;
}

输出:

vector 保存原顺序:3 1 3 2 
set 有序去重:1 2 3 
map 逐项计数:1:1 2:1 3:2 
queue 先进先出:3 1 3 2 
priority_queue 最大值优先:3 3 2 1 

同一份输入,差别不在“算了什么”,而在你从容器里观察到的是什么:存储内容、计数结果,还是取出顺序。

一步步看变化

容器 你观察的是 [3,1,3,2] 进去之后看到 为什么
vector 存储内容 3 1 3 2 按原顺序保存,重复值也照样留着
set 存储内容 1 2 3 自动去重并升序排列
map 计数结果 1:1 2:1 3:2 把每个值当键,累加出现次数
queue 取出顺序 3 1 3 2 先进先出,和入队顺序相同
priority_queue 取出顺序 3 3 2 1 每次取出当前最大值

读这张表时要分清两件事:

  • vector 和 set 比较的是存下来什么。
  • queue 和 priority_queue 比较的是取出来什么顺序(queue 里确实还存着 3 1 3 2,只是取出次序恰好一致;priority_queue 每次弹最大)。
  • map 记录的是统计结果,原来的顺序和重复都不重要了。

逐项扩展核心操作

容器选择表

需求 选它 链接
按下标随机访问,主要在尾部增删 vector vector 动态数组
两端都要能高效进出 deque deque 双端队列
已知位置时频繁在中间插删 list list 链表
后进先出 stack stack 栈
先进先出 queue queue 队列
每次取最大/最优元素 priority_queue priority_queue 优先队列
有序且不重复 set set 有序集合
有序且允许重复 multiset set 有序集合
按键找到对应的值 map map 键值映射
一个键对应多条记录 multimap multimap 一键多值
两个相关值放在一起 pair pair 二元组
处理字符串 string string 字符串
一组二进制开关 bitset bitset 位集

算法选择表

任务 选它 前提 链接
找出某个值在不在、在哪 find 无 find 查找
找第一个满足条件的元素 find_if 需要一个判断条件 find 查找
把数据按规则排序 sort 无 sort 排序
排序且保持同值原顺序 stable_sort 无 sort 排序
判断有序数据里有没有某值 binary_search 区间已按同一规则有序 二分查找
找第一个不小于 x 的位置 lower_bound 区间已有序 二分查找
找第一个大于 x 的位置 upper_bound 区间已有序 二分查找
合并相邻重复 unique 重复值已经相邻 unique 去重
全体数值去重 sort + unique + erase 允许改变顺序 unique 去重
找第 k 小 nth_element 1 <= k <= n nth_element 第 k 小

复杂度速查

操作 复杂度 说明
vector 下标访问 O(1) 连续存放,直接定位
vector 尾部 push_back / pop_back 摊还 O(1) 偶尔扩容较贵,摊到多次操作上仍接近常数
vector 中间 insert / erase O(n) 后面的元素要整体搬动
list 已知位置的 insert / erase O(1) 但“找到那个位置”仍要 O(n)
set / map 查找、插入、删除 O(log n) 树结构维护有序
priority_queue push / pop O(log n) top 是 O(1)
stack / queue 的 push / pop / front O(1) 只在端点操作
sort O(n log n) 不保证同值原顺序
find O(n) 从头线性查找
binary_search / lower_bound / upper_bound O(log n) 前提是区间有序
unique O(n) 顺着走一遍
nth_element 平均 O(n) 只保证第 k 小就位,不完成整体排序

什么时候选它、哪里容易错

unique 不是“全体去重”。 它只合并相邻的相等元素,不排序直接调用,[1,2,1] 一个也去不掉。要全体去重,得自己加 sort 把相等的值聚到一起。见 unique 去重。

sort 不保证同值元素的原顺序,stable_sort 才保证。 所以不要断言“不稳定排序后同分的次序是某某”;要保序就用 stable_sort。见 sort 排序。

map 的 operator[] 查不到会插入。 m[key] 在键不存在时会插入一个默认值,size() 变大;只查询用 find / count,它们不会插入。见 map 键值映射。

binary_search 返回的是布尔值,不是位置。 要位置得用 lower_bound / upper_bound,而且区间必须已经有序。见 二分查找。

priority_queue 的比较规则和 sort 方向相反。 sort 的规则写“谁在前”,priority_queue 的规则决定“谁在顶”;默认 less 时 sort 升序、priority_queue 取最大值。见 priority_queue 优先队列。

queue / stack 没有迭代器,不能遍历。 它们只开放两端操作,想看内容只能不断取出(或打印副本)。见 queue 队列、stack 栈。

set 的元素是常量,不能改。 想“改”一个键,只能删掉再插入。见 set 有序集合。

插删之后旧迭代器可能失效。 尤其 vector 扩容或中间插删时,之前记下的迭代器要重新获取。见 容器、迭代器与区间。

操作速查表

你的任务 一句话选择
按下标存一批数,长度不定 vector
两端进出 deque
已知位置频繁插删 list
后进先出 stack
先进先出 queue
每次要最大 / 最优先 priority_queue
去重并保持有序 set
按键找值 map
一个键多条记录 multimap
查找值 / 找第一个满足条件的 find / find_if
排序 / 保序排序 sort / stable_sort
在有序数据里查边界 binary_search / lower_bound / upper_bound
相邻去重 / 全体去重 unique / sort + unique + erase
找第 k 小 nth_element

动手检查:选择自测

先自己选,再展开折叠核对。

  1. 保存重复记录:要读入 n 条成绩,其中可能有相同的分数,输出时还要保持输入顺序。用哪种容器?
  2. 按先来后到处理:有一批任务按到达顺序排队,必须严格按到达先后处理。用哪种容器?
  3. 统计出现次数:读入一串数,最后要输出每个不同的数出现了几次。用哪种容器?
  4. 找第 k 小:只想知道第 k 小的值是多少,不需要把整串数排好。用哪个算法?
参考结果与解释
  1. vector:它按原顺序保存,也不去重,正好符合“保留重复、保持顺序”。用 vector 一路 push_back 即可。
  2. queue:先进先出。用 queue 读入时 push,处理时 front + pop,取出顺序就是到达顺序。
  3. map:把每个数当键,遇到一次就 cnt[x]++,最后遍历输出键和计数,是逐项统计的标准做法。见 map。如果只关心“出现过没有”,用 set 就够了。
  4. nth_element:它让第 k 小的元素就位,但不把整串排好,平均 O(n),比整体 sort 的 O(n log n) 更快。见 nth_element。验证下面程序:
#include <bits/stdc++.h>
using namespace std;

int main() {
    vector<int> v = {7, 2, 5, 1, 9};
    int k = 3;

    nth_element(v.begin(), v.begin() + (k - 1), v.end());
    cout << "第 " << k << " 小 = " << v[k - 1] << '\n';
    return 0;
}

输出:

第 3 小 = 5

立即练习

本页是总结与选择页,重点在“根据需求选工具”,已经有选择自测;具体的 OJ 题目按各个专题页的「立即练习」安排,这里不再重复。