STL容器基础 知识点讲解

信息学竞赛(CSP-J/S、NOIP、GESP)高频考点精讲:从基础规则到推演与易错点,配真题例题。学完可一键去 OI伴学在线刷题。

去 OI伴学在线练习 →

本讲学习目标

概念

STL(Standard Template Library,标准模板库)是 C++ 自带的一整套“现成数据结构和算法”。你可以把它想象成工具箱里已经磨好、可以直接拿出来的锤子、螺丝刀和扳手,而不是让你每次去铁匠铺重新打一把。竞赛里最常用的是容器(装数据的盒子)和算法(sort 之类)。

本讲重点讲三个最容易上手、也最常考的:

1. vector:会变长的数组(动态数组),最常用的容器;

2. map / set:自动按 key 排序、能 O(log n) 查找和去重;

3. string 与 sort 自定义比较:字符串处理 + 按规则排序。

先说结论再展开:STL 不是“黑魔法”,它本质就是别人帮你写好的 数组 / 链表 / 平衡树。你用 vector 就是用一个会自动扩容的数组;用 set 就是用一个“插入后自动有序、自动去重”的红黑树。学会“什么场景选哪个容器”,比死记每个函数更重要。

触发清单(看到这些信号第一时间想到本知识点)

题干信号优先考虑什么对应思路
“读一堆数”“数量不固定”“边读边存”vector定义 vector<T>,用 push_back 填入,最后 sort 或按索引取
“排个序”“按某某顺序输出”sort + cmp默认升序;多关键字需写比较函数,注意区间左闭右开
“去重”“有没有出现过”“出现几次”set / mapset 自动去重且有序;map 按 key 统计次数
“字符串截取”“拼接”“字典序比较”string用 substr、+=、> < 比较,别用 char 数组硬抠
“n ≤ 100 万但只关心前 k 小/大”nth_element / 优先队列O(n) 找第 k 小,不必全排序

定义

推演 1 · vector 为什么“能当数组用,还能自动变长”

vector 内部维护一段连续内存,还记着“当前长度”和“容量”。当你 push_back 但容量不够时,它会申请一块更大的内存、把旧数据拷过去、释放旧的。所以你写代码时永远当它是数组:a[0]、a[1]、a.size() 都能用,但它比裸数组多了一个“会长大”的能力。

用法:凡是“数量未知的同类数据”,第一反应就是 vector<int> a;,别再开 int a[100005] 赌大小。

推演 2 · sort 到底按什么排,为什么必须写 cmp

sort(a, a+n) 调用 < 比较。对 int 就是数值大小;对 string 是字典序(按位比 ASCII)。但当你要对“结构体按成绩排、成绩相同按学号排”时,编译器不知道你的规则,必须你写:

bool cmp(const Node& x, const Node& y) {
 if (x.score != y.score) return x.score > y.score; // 成绩高在前
 return x.id < y.id; // 否则学号小在前
}
sort(a, a + n, cmp);
用法:return a < b 表示"a 应该排在 b 前面吗”。这是几乎所有自定义排序题的骨架(拼数、奖学金、分数线划定全靠它)。

推演 3 · set / map 为什么“插进去就自动有序、还能 O(log n) 查”

它们底层是红黑树(一种自平衡二叉搜索树)。插入时树会自动调整形状保持“左小右大”,所以遍历就是有序的;查找时走“比大小往左/右走”的路径,最多走树高 = O(log n)。set 的 insert 若 key 已存在就忽略 → 天然去重;map 用 m[k] 直接读写“键 k 对应的值”,特别适合“统计每个单词出现次数”。

用法:看到“是否出现过 / 出现了几次 / 按 key 取”,先想 set / map,不要手写哈希或排序去重。

推演 4 · string 与 sort 的字典序陷阱

string 比较是按字典序:先比长度,长度相同比每一位的 ASCII 码。这意味着 "b" 比 "aa" 大,因为 'b' > 'a'。如果你要按“长度优先,长度相同再按字典序”排,必须写 cmp:

bool cmp(const string& a, const string& b) {
 if (a.size() != b.size()) return a.size() > b.size(); // 长的在前
 return a < b; // 同长按字典序
}
用法:别直接用 sort(strs),否则短串可能排在长串前面。

技巧速查表(背下来)

想做的事写法说明
动态数组vector<int> a; a.push_back(x);自动扩容,下标从 0 开始
排序sort(a.begin(), a.end());默认升序,区间左闭右开
自定义排序sort(a, a+n, cmp);cmp 返回 true 表示左排右前
去重unique(a.begin(), a.end()) - a.begin();需先 sort,返回新逻辑长度
判重/计数map<string,int> m; m[k]++;m[k] 自动插入缺键
第 k 小nth_element(a.begin(), a.begin()+k, a.end());O(n),a[k] 就位
字符串拼接s += "abc";高效,避免 s = s + "abc" 的临时对象
字符串截取s.substr(pos, len);从 pos 起取 len 个字符

原讲义配图

例题

例 1:动态数组去重排序

题目描述

输入一个正整数 $n$($1 \le n \le 10^5$),接着输入 $n$ 个整数 $a_1, a_2, \dots, a_n$($-10^9 \le a_i \le 10^9$)。请将这 $n$ 个数从小到大排序,并去掉重复的数,最后输出剩下的数,每行一个。

审题分析

解题思路

1. 定义 vector<int> a(n) 读入 $n$ 个数。

2. 调用 sort(a.begin(), a.end()) 升序。

3. 调用 unique(a.begin(), a.end()) 把重复元素移到末尾,返回迭代器指向“新逻辑末尾”。

4. 用减法算出新长度 m,只输出前 m 个元素。

C++ 完整参考代码

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main() {
 // 读入 n
 int n;
 cin >> n;

 // 定义动态数组,大小为 n
 vector<int> a(n);
 for (int i = 0; i < n; i++) {
 cin >> a[i]; // 像普通数组一样按索引读入
 }

 // 排序:升序,区间 [begin, end)
 sort(a.begin(), a.end());

 // unique 把重复元素移到末尾,返回新逻辑末尾迭代器
 auto last = unique(a.begin(), a.end());
 // 计算去重后的有效长度
 int m = distance(a.begin(), last); // 等价于 last - a.begin()

 // 只输出前 m 个不重复元素
 for (int i = 0; i < m; i++) {
 cout << a[i] << '\n';
 }

 return 0;
}

代码解读(含真实运行输出)

输入:

7
1 2 2 3 3 3 4

程序执行过程:

运行结果

1
2
3
4

---

例 2:字符串展开

题目描述

给定一个字符串 $s$(仅包含小写字母、数字、冒号 :、加号 +、减号 -),按如下规则展开:

请输出展开后的字符串。

审题分析

解题思路

1. 读入字符串 s。

2. 遍历 s,当发现 s[i] == 'a' 且 s[i+1] == '-' 时:

- 看 s[i+2] 是什么:

- 若是字母,则从 'a' 到该字母(含)逐个输出;

- 若是数字,则输出对应个数(注意数字可能大于 26,题目隐含只输出 26 个);

- 若是 + 或 :,则输出 26 个字母。

3. 其他字符直接输出。

4. 注意不要越界访问 s[i+2]。

C++ 完整参考代码

#include <iostream>
#include <string>
using namespace std;

int main() {
 string s;
 cin >> s;

 for (int i = 0; i < s.size(); i++) {
 if (s[i] == 'a' && i + 1 < s.size() && s[i + 1] == '-') {
 // 识别到 a - x 模式
 char x = s[i + 2];
 int count = 0;
 if (x >= 'a' && x <= 'z') {
 // 字母:从 a 到 x
 for (char c = 'a'; c <= x; c++) {
 cout << c;
 }
 } else if (x >= '0' && x <= '9') {
 // 数字:输出对应个数(最多 26 个)
 count = x - '0';
 for (int j = 0; j < count && j < 26; j++) {
 cout << (char)('a' + j);
 }
 } else {
 // + 或 : 输出 26 个字母
 for (int j = 0; j < 26; j++) {
 cout << (char)('a' + j);
 }
 }
 // 跳过已处理的三个字符
 i += 2;
 } else {
 cout << s[i];
 }
 }
 cout << '\n';
 return 0;
}

代码解读(含真实运行输出)

输入:

a-b
a-10
a-+
a-:

程序执行:

运行结果

a b
a b
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz

例题复盘总结

共性特征

各自技巧

审题模板(固定思考流程)

1. 数据量:是否超过 $10^5$?→ 必须用 vector/string,不能用 char 数组。

2. 操作类型:排序?去重?字符串展开?→ 对应 sort/unique/循环生成。

3. 边界条件:下标越界?空字符串?数字多位?→ 加 i+1 < s.size() 等检查。

4. 输出格式:每行一个?空格分隔?→ 注意 \n 或 。

错误清单(特有)

写法总表

技法分层解决什么典型写法
基础容器动态数组、字符串vector<T> a; a.push_back(x);
string s; s += "abc";
排序与去重升序/降序、去重sort(a.begin(), a.end());
unique(a.begin(), a.end()) - a.begin();
自定义排序多关键字、结构体sort(a, a+n, cmp);
bool cmp(const Node& x, const Node& y) { ... }
关联容器判重、计数、按 key 取map<string,int> m; m[k]++;
set<int> s; if(s.count(x)) ...
字符串展开按规则生成中间字符循环从 'a' 到目标字符,或按数字生成

分工说明:触发清单用于选题(看到什么信号想到什么容器),技巧速查表用于编码(具体函数怎么写),写法总表用于归类(不同技法对应不同需求)。

常见错误

1. sort 区间写错:写成 sort(a, a+n-1) 会漏掉最后一个;正确是 a, a+n(或 a.begin(), a.end())。

2. cmp 写错方向:return x < y 才是升序;想要降序要 return x > y。cmp 返回 true 表示"x 排在 y 前”。

3. cmp 用 <= 或 >=:比较函数必须严格弱序,写成 <= 会在相等时返回 true,导致 sort 行为未定义(可能 RE/死循环)。

4. 混淆 set 去重和“保留重复”:要去重才用 set;要保留重复个数用 multiset 或 map<值,次数>。

5. vector 下标越界:a[i] 不检查边界,循环条件写错就 RE;多用 size() 和范围 for。

6. map 的 [] 会自动插入:if (m[k]) 这种判断会先把 k 插进去变成 0,想“只查不插”用 m.count(k) 或 m.find(k)。

7. string 下标也越界:s[i] 的 i 必须 < s.size()。

自测

1. vector 和裸数组 int a[100] 最大的区别是什么?为什么“数量未知”时优先用 vector?

2. sort(a, a+n) 默认是升序还是降序?要把结构体按成绩降序、同分按学号升序,cmp 怎么写?

3. 为什么 cmp 里不能写 return x.score <= y.score?会有什么后果?

4. set 和 map 的查找复杂度是多少?它们底层是什么结构?

5. 要“统计每个名字出现了几次”用哪个容器、哪个操作?

6. unique(a.begin(), a.end()) 真的会删除重复元素吗?去重的标准三步是什么?

7. 题目说“n ≤ 10⁶,求动态的第 k 小”,你会想到哪个 STL 算法或容器?

举一反三 · 审题流水线

拿到一道题,先问自己三个问题:

练手题

题 A:单词计数

输入一个正整数 $n$($1 \le n \le 10^5$),接着输入 $n$ 个单词(每个单词由小写字母组成,长度不超过 100)。请统计每个单词出现的次数,并按字典序从小到大输出,格式为“单词 次数”,每行一个。

题 B:动态第 k 小

输入一个正整数 $n$($1 \le n \le 10^6$)和一个整数 $k$($1 \le k \le n$),接着输入 $n$ 个整数 $a_1, a_2, \dots, a_n$($-10^9 \le a_i \le 10^9$)。请输出这 $n$ 个数中第 $k$ 小的数。

学完这个考点,用历年真题检验一下

OI伴学 把「STL容器基础」在 CCF-GESP / CSP / NOIP 真题里出现过的题目汇总到一页,可按考点横向刷;每题都带答案、解题思维和常见错误。

看 STL容器基础 历年真题汇总 → 去平台刷题 →