序列容器按"位置"存东西(v[3] 是"第 4 个"),关联式容器按"键"存东西(m["张三"] 是"姓张的那个人")。 它内部的元素始终按某个规则自动排好序(默认从小到大),所以插入、查找、删除都是 O(log n)——比"从头找一遍"的 O(n) 快得多。四个成员两两成对:set / multiset(只存键)、map / multimap(键 + 值),而每一对里的差别只有一条:许不许键重复。
13.1 的三个盒子(vector / deque / list)都是"排队"思路:元素有先后、位置有意义、可以按下标找。
这一节换了一种思路:"名录"。你查电话簿不会从第一页翻到最后——你直接翻到"张"字那一栏。关联式容器就是把"翻到那一栏"这件事自动化了。
它的底层是"平衡二叉搜索树"(教材不展开,你只要知道这个结构能保证"每次比较都能砍掉一半候选")——这跟第七章讲的数组完全不同,也和 10.4.5 嵌套类那种"藏起的辅助结构"是一脉相承的工程做法。
一个伏笔:这一节反复出现的 pair,就是 12.2.2 的 Pair<T1, T2> ——标准库的那个版本叫 std::pair。
一、两种思路的根本差别
先看同一个需求,两种容器的写法:
// 需求:记住每个人的年龄,之后按名字查
// ── 序列容器的做法:自己找 ──
vector<pair<string, int>> v;
v.push_back(make_pair("张三", 20));
v.push_back(make_pair("李四", 22));
// 查"李四"的年龄:得写个循环一个个比
int age = -1;
for (size_t i = 0; i < v.size(); i++)
if (v[i].first == "李四") { age = v[i].second; break; }
// ── 关联式容器的做法:它自己知道该在哪 ──
map<string, int> m;
m["张三"] = 20;
m["李四"] = 22;
int age2 = m["李四"]; // ★ 一句就到手
差别的本质:序列容器只保证"位置",你得自己知道"东西放在第几个";关联式容器自己维护"按什么排",你只需要给出"键"。
| 对比项 | 序列容器(13.1) | 关联式容器(13.2) |
|---|---|---|
| 元素的"身份" | 由位置决定 | 由键决定 |
| 能不能按下标取 | 能(v[3]) |
不能(没有"第 3 个"这个概念) |
| 元素顺序 | 你放进来的顺序 | 它自己排好(默认升序) |
| 查找一个值 | O(n)(find 从头扫) |
O(log n)(底层是平衡二叉树) |
| 能不能有重复 | 能(随便重) | set / map 不行,multiset / multimap 可以 |
| 改一个已插入的元素 | 直接改(v[0] = x) |
不行(改了顺序就乱了;要"删了再插") |
| 迭代器类型 | 三兄弟各不同 | 一律双向(13.4.4) |
最后两行很关键,顺序记不住就容易犯错: - 不能改元素:
set里的值就是它的"排序依据",一改就破坏有序性 → 只能erase再insert。 - 迭代器只有双向:所以set/map也不能用std::sort(因为它们本来就一直是排好序的,根本不需要排序)。
二、四个成员:一张表看全
| 容器 | 存什么 | 键能重复吗 | 有序吗 | 一句话用途 |
|---|---|---|---|---|
set(13.2.1) |
只有键 | ❌ 唯一 | ✅ 自动升序 | 去重 + 自动排序(比如"统计有哪些不重复的学科") |
multiset(13.2.2) |
只有键 | ✅ 可以重复 | ✅ 自动升序 | 有重复的"排序袋"(比如"全班所有分数,含重复") |
map(13.2.3) |
键 + 值 | ❌ 键唯一 | ✅ 按键升序 | 字典 / 名录(人名 → 电话) |
multimap(13.2.4) |
键 + 值 | ✅ 键可重复 | ✅ 按键升序 | 一对多(作者 → 他的多本书) |
"两两成对"的含义:set 和 multiset 的差别只有"允不允许重复";map 和 multimap 也一样。所以学一个,另一个就是"加一条允许重复"。
那 set 和 map 的区别呢? ——map 比 set 多存了一个"值"。可以这样理解:set 的每个元素是 key,map 的每个元素是一个 pair<const key, value>。
set<string>: {"数学", "语文", "英语"}
↑ 每个元素就是一个键
map<string,int>: { {"数学", 95}, {"语文", 88}, {"英语", 92} }
↑ 每个元素是"键 + 值"的一对
三、动手 Demo:同一份数据,两种存法
任务:统计一段文字里出现过哪些不重复的词,以及每个词出现的次数。
#include <iostream>
#include <set>
#include <map>
#include <sstream>
#include <string>
using namespace std;
int main() {
string text = "apple banana apple cherry banana apple";
// ── ① 去重(set 最典型的用法)──
set<string> words;
stringstream ss(text);
string w;
while (ss >> w) words.insert(w); // ★ 自动去重 + 自动排序
cout << "不重复的词有 " << words.size() << " 个:";
for (const string &s : words) cout << s << " ";
cout << endl;
// ── ② 计数(map 最典型的用法)──
map<string, int> cnt;
stringstream ss2(text);
while (ss2 >> w) {
cnt[w]++; // ★ 没见过就自动建一个(值默认 0),再 +1
}
cout << "词频:" << endl;
for (const map<string, int>::value_type &p : cnt) // ★ value_type = pair<const string, int>
cout << " " << p.first << " -> " << p.second << endl;
return 0;
}
看到什么算成功:
不重复的词有 3 个:apple banana cherry
词频:
apple -> 3
banana -> 2
cherry -> 1
两处"它就替我做了":
| 现象 | 说明 |
|---|---|
words 直接就是 apple banana cherry(不是原文顺序) |
set 自动按字典序排好了——原文是 apple/banana/cherry 出现顺序不同,输出却是排好序的 |
cnt[w]++ 一句就完成了"没有就建、有就加一" |
map::operator[] 访问一个不存在的键时会自动插入一个默认值。这是 map 最方便、也最容易踩的一个特性(见 13.2.3) |
注意
map遍历时那个长类型:map<string, int>::value_type就是pair<const string, int>。键是const的——这正是"不能改键"的语法体现。C++11 之后可以写const auto &p : cnt,省掉这一长串。
30 秒实验
- 改一下输入顺序:把
text改成"cherry apple banana",重跑 → 输出顺序完全一样(set/map都是自动排序的)。 set和multiset对比:把set<string> words;改成multiset<string> words;重跑 →size()变成 5,apple出现了三次。这就是两兄弟唯一的差别。- 验证"不能改元素":写
*words.begin() = "x";→ 报assignment of read-only location(因为set的元素是const的)。 - 验证"没有下标":写
words[0]→ 编译不过(set没有operator[])。注意:map有operator[],但那个参数是"键"不是"下标"。
常见误区
- "
set/map也能用sort"——不能(迭代器只有双向)。但它们本来就有序,不需要排。 想换排序规则,要在定义时给比较函数(13.2.3)。 - "关联式容器比序列容器快,所以都该用它"——看操作:按值查找、去重、排序它快(O(log n) vs O(n));按下标遍历、频繁在尾部追加它反而慢(树结构到处跳,缓存不友好,且每个节点有额外开销)。"要按位置取"的场景,
vector才是正解。 - "顺序容器就是无序,关联式容器才有顺序"——说反了一半:序列容器有"你放进来的顺序",关联式容器有"排好的顺序"。 两个都有顺序,只是" 谁决定顺序"不同。
- "改了
set里的元素也没关系"——编译期就会拦你(元素是const)。要改就erase+insert——这个动作还顺便保证它在新的位置上排序正确。 - "关联式容器里放的键,什么类型都行"——要求这个类型"能用
<比较"(因为它要靠比较来建树)。存自定义类时,要么给它重载<(11.2),要么在定义容器时给一个比较函数。 否则报一大串看不懂的模板错误(12.4 小结说的"模板报错看最后一条 note"在这里特别有用)。 - "
set和unordered_set是一回事"——不是。set有序、O(log n);unordered_set(哈希表)无序、平均 O(1)。要"有序"或"按顺序遍历"就用set;只要"快速去重/查找"用unordered_set更快。 教材只讲有序版,认得这个名字就好。
自测三问(点开对答案)
1. **序列容器和关联式容器最根本的差别是什么?**——**元素的"身份"从哪来**:序列容器靠**位置**("第 3 个"),关联式容器靠**键**("叫张三的那个")。所以**前者有 `[]`、能按下标;后者没有 `[]`(`map` 的那个"下标"其实是键),而且元素始终按规则自动排序。** 2. **四个成员怎么记?**——先按"存不存值"分成两对:**`set` / `multiset` 只存键**,**`map` / `multimap` 存"键 + 值"**。每一对里再加一条:**名字带 `multi-` 的那个允许键重复,不带的不允许**。**所以真正的差别只有两条:存不存值、允不允许重复。** 3. **为什么 `set` 里的元素不能改?**——因为**它就是排序的依据**:改了它,树的顺序就不成立了(二叉搜索树的一切操作都依赖"左小右大")。**所以 `set` 的元素是 `const` 的,改只能"先删再插"。**- ☐ 下一节(13.2.1 set类模板)