课程 · C++ 速查 · STL 专题 · 第 8 块

map:键 → 值的一张表

学会「键 → 值」的 map:计数、映射、增查删遍历,最要紧的是分清 m[k] 和 m.count(k)。

第 8 块(共 12 块已写好)8 条动作2 处陷阱2 道例题2 道练习4 份可运行代码

读这一页之前,先知道读完要能做什么

  • 写出来计数三句:cnt[x]++、遍历按键从小到大、count 查在不在
  • 不犯那个最常见的错:只想查一下却写了 m[k],把表改了
  • 知道 find 找不到时返回 m.end(),取值前先判

这是什么

3 段

一句话:map 是「下标可以不是整数的数组」。map<string, int> cnt; 之后 cnt["cat"]++; —— 键不存在就自动建一个,值从 0 开始。这就是计数的全部秘密。

★ 键是字符串、是 pair 都行,所以编号很大、下标不是整数时它就是标准解法 —— 「开不出那么大的数组」时别忘了它。

★★ map 和第 9 块的 set 一样有序、查找是 O(log n),lower_bound(第 5 块)那套规矩也原样适用。

完整动作表

8 条

复杂度写的是这个动作自己的代价,不是整个程序的。「这一栏最容易错的地方」那一列是重点。

写法干什么代价
map<K, V> m;开一张表(map<string, int>、map<int, int> …)
★ 「K」是键的类型,「V」是值的类型
O(1)
m[k]++计数:键不存在就先建一个(值从 0 开始),再 +1
★★★ 三句里的第一句;⚠ 想「只查不建」别用它(陷阱 ①)
O(log n)
m[k] = v;把键 k 的值写成 v(键不存在就建)
★ 和数组下标赋值一个写法
O(log n)
m.count(k)键 k 在不在(返回 0 或 1)
★★★ 只想知道「有没有」就用它 —— 它不会建键
O(log n)
m.find(k)找键 k;找不到返回 m.end()
★★ 拿值之前先判 != m.end()(陷阱 ②)
O(log n)
m.erase(k)删掉键 k(没有就什么都不做)O(log n)
for (auto &[k, v] : m)遍历整张表
★★★ 出来按键从小到大;老写法是 for (auto &kv : m) + kv.first / kv.second
O(n)
m.size() / m.empty()里面几张记录 / 一条都没有吗
⚠ 和 int 比大小写 (int)m.size()
O(1)

⭐ 这一节最容易错的地方(2 处)

这一节排在例题前面是故意的:读到一个坑,再去看正确的例子,记得住。 下面每一条都是「编译得过去、答案可能是错的」那种 —— 它们才是 OI 里真正扣分的地方。

① 只想「查一下」,却写了 `m[k]` —— 它会顺手把键造出来

m[k] 的意思是「给我这个键的值,没有就先造一个」。所以 if (m["ghost"] > 0) 这种判断跑完,表里就多了一个键,m.size() 变了。 ★ 于是遍历时冒出一个你没放过的键、计数莫名其妙多 1、「明明没交过」的人被算成交过 —— 不报错,答案错。
例题 ② 的实测输出(同一份程序里两种写法对着跑)
① 用 count 查 cnt[999](不存在):0,size 还是 3   ← 只查不建
② 用 cnt[999] 查同一个键:值 0,size 变成 4   ← [] 顺手把它造了出来
改成
// 只想查在不在
if (m.count(k)) { ... }

// 想拿值,又不想建键
auto it = m.find(k);
if (it != m.end()) use(it->second);
★★★ 只查在不在 → count;要拿值 → 先 find 再判 end()。

② `find` 找不到返回 `m.end()` —— 解引用它就崩

find 返回的是一个位置,不是值。找不到时它给的是 end():「最后一个之后」那一处,不指向任何元素。 ★ 直接 *it 去取值,编译器一个警告都不给 —— 运行时要么 Segmentation fault,要么拿到一个看起来挺合理的数。
改成
auto it = m.find(k);
if (it != m.end()) {          // ★ 先判
  use(it->second);
}
★ 和第 5 块那条规矩一样:拿到位置,先判是不是 end()。

本书哪一章真的用到它

2 处

⚠ 下表里的文件路径和那串 probe 是逐字验过的(check:docs 第 ⑳ 条)—— 改了那份代码这儿会红,正是要的。点章号直接去看那一页。

章书里的哪一份代码用它来干什么
第 45 章code/45-estimate/p1786Check.cpp★★ 把名字映射成编号 —— 「开不出那么大的数组」时的标准解法
第 49 章code/49-hash/p1381.cpp★ 同一题的哈希版:unordered_map 换个名字、用法一样,速查里单有一条

例题(2 道)

每道都给全套:题面 → 样例 → 参考代码 → 一步步的讲解。先自己读题想一遍,再看代码。

例题 ① 计数三句,外加「查一下」的两种写法

map-count.cpp

读 n 个整数,数出每个数出现了几次(按键从小到大输出),然后用两种写法各查一个不存在的键,看看表有没有被改。

★ 这一题的重点在后半段:同一个查询,count 和 [] 的后果不一样。

输入格式第一行 n。第二行 n 个整数。
输出格式每个数的出现次数,两种查询各自的返回值和 size 变化,最后再遍历一遍。

输入

6
3 1 3 5 1 3

输出

读进 6 个数,map 里有 3 个键:
1 -> 2
3 -> 3
5 -> 1

① 用 count 查 cnt[999](不存在):0,size 还是 3   ← 只查不建
② 用 cnt[999] 查同一个键:值 0,size 变成 4   ← [] 顺手把它造了出来

现在再遍历一遍,多出来的那个键就在里面:
1 -> 2
3 -> 3
5 -> 1
999 -> 0
3 出现 3 次、1 出现 2 次、5 出现 1 次 —— 输出是按键从小到大的。查 999 时:count 之后 size 还是 3,[] 之后变成 4,而且最后那次遍历里真的多了一个 999 -> 0。
先想一想

★ 计数只要一句 cnt[x]++;。⚠ 关键是想清楚 cnt[999] 这句话除了返回值还干了什么。

map-count.cpp参考代码
// STL 专题 · map —— 计数三句,外加「查一下」的两种写法
//
// ★ 计数的全部秘密就是这一句:
// cnt[x]++; // 键不存在就先建一个,值从 0 开始
//
// ⚠★★ 这一份专门演示陷阱:**只想查一下,却写了 m[k]** ——
// `[]` 的意思是「给我这个键的值,**没有就先造一个**」。
// 于是查一个不存在的键,size 就多 1。它不报错,只是把表改了。
// 想「只查不建」,用 count(或者先 find 再判 end())。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
map<int, int> cnt;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
cnt[x]++; // ★ 三句里的第一句:见到就 +1
}
cout << "读进 " << n << " 个数,map 里有 " << cnt.size() << " 个键:\n";
for (auto &[k, v] : cnt) cout << k << " -> " << v << "\n"; // ★ 按键从小到大
cout << "\n① 用 count 查 cnt[999](不存在):"
<< cnt.count(999) << ",size 还是 " << cnt.size() << " ← 只查不建\n";
cout << "② 用 cnt[999] 查同一个键:值 " << cnt[999]
<< ",size 变成 " << cnt.size() << " ← [] 顺手把它造了出来\n";
cout << "\n现在再遍历一遍,多出来的那个键就在里面:\n";
for (auto &[k, v] : cnt) cout << k << " -> " << v << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 计数一句:cnt[x]++; —— 键没见过就先建出来(值从 0 开始),再 +1。
  2. 遍历:for (auto &[k, v] : cnt) —— 结构化绑定,出来按键从小到大,所以 1、3、5 这个顺序不是巧合。
  3. ★★ ① cnt.count(999):返回 0,size 没动 —— 只查不建。
  4. ★★ ② cnt[999]:也返回 0,但 size 从 3 变成 4 —— [] 顺手把键造出来了,最后那次遍历里 999 -> 0 就是证据。这就是陷阱 ①。
记住这一句

★★★ m[k] 是「取值,没有就造一个」,m.count(k) 是「只问有没有」。计数用前者,查询用后者 —— 用错了不报错,只是表被改了。

用到的写法:m[k]++ m.count(k) for (auto &[k, v] : m) m.size()

例题 ② 名字 → 编号:查不到的别造出来

map-id.cpp

一场比赛 n 个人报名(可能重名),编号按第一次出现的顺序从 0 开始。然后回答 m 个查询:这个名字编号是多少?没报名过的输出 -1。

★ 这是 map 计数之外的另一种标准用法:一张「名字 → 编号」的映射表 —— 第 45 章处理名字的那道题就是这么开的表。

输入格式第一行 n。接下来 n 行报名名字。然后 m。接下来 m 行查询名字。
输出格式每个查询一行:名字的编号,没报过名输出 -1。

输入

5
cat
dog
cat
emu
dog
3
cat
bat
emu

输出

0
-1
2
cat 第一次出现排 0,dog 1,emu 2;重名的 cat 不重发编号。查 bat 没见过,输出 -1。
先想一想

★ 报名阶段只给没见过的名字编号,查询用 find —— ⚠ 别用 m[name] 读:没见过的名字会被顺手造出来,编号就乱了。

map-id.cpp参考代码
// STL 专题 · map 例题 —— 名字 → 编号
//
// ★ map 计数之外的另一种标准用法:名字到编号的映射。
// 报名阶段只给没见过的名字编号(m[k] 的「建」正好用上),
// 查询阶段一律 find —— m[q] 会把没报过名的造出来。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
map<string, int> id;
for (int i = 0; i < n; i++) {
string name;
cin >> name;
if (!id.count(name)) id[name] = (int)id.size(); // ★ 没见过才登记,编号取当前表大小
}
int m;
cin >> m;
for (int i = 0; i < m; i++) {
string q;
cin >> q;
auto it = id.find(q); // ★ 读一律 find,不用 m[q]
if (it == id.end()) cout << -1 << '\n';
else cout << it->second << '\n';
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
这段代码是怎么想出来的
  1. 报名:if (!id.count(name)) id[name] = (int)id.size(); —— 没见过才登记,编号取当前表大小。
  2. 查询:auto it = id.find(q); if (it == id.end()) cout << -1; else cout << it->second;
  3. ★ 两个阶段都不要在读的时候用 id[q] —— 见陷阱 ①,建出来的那个键会污染表。
  4. 遍历出来按名字字典序,不是报名顺序 —— 想按编号序输出就得另存一份。
记住这一句

★★★ 「给东西编号 / 查编号」就是 map 的映射用法,读一律 find 先判 end()。

用到的写法:map<string, int> id; id.count(k) id.find(k) it->second

练习(2 道)

只给题面、样例和一句提示,答案是折叠着的 —— 先自己写一版。

练习 ① 谁出现得最多:计数 → 遍历 → 跟着记最大

map-max.cpp

给 n 个学号(可能重复),输出每个学号出现的次数,以及交得最多的那个学号(并列时取学号小的)。

★ 这是 map 的标准三段式:计数 → 遍历 → 跟着记一个「当前最多」。

输入格式第一行 n。第二行 n 个学号。
输出格式每个学号的次数(按键从小到大),以及最多次数和那个学号。

输入

9
5 3 5 8 3 5 9 3 5

输出

每个学号出现的次数(按键从小到大):
3 -> 3
5 -> 4
8 -> 1
9 -> 1

交得最多的是 5,交了 4 次
一共 4 个学号交过,记录一共 9 条
提示

★ 计数一句就够。⚠ 关键在并列时怎么取「学号小的」—— 想想 map 遍历出来的顺序,再决定比较写 > 还是 >=。

map-max.cpp参考答案
记住这一句

★★★ map 遍历出来按键从小到大,所以「并列取小的」只要写严格大于 > —— 先见到的那个就守住了;写 >= 会变成后面那个赢。

用到的写法:m[k]++ for (auto &[k, v] : m) m.size()

练习 ② 模拟一本通讯录:add / ask / del

map-ops.cpp

从空开始处理 m 个操作:add name tel(存电话)、ask name(输出电话,没有就 not found)、del name(删掉)。

★ 重点是三个动作各归各位:存用 m[k] = v,问走 find,删用 erase。

输入格式第一行 m。之后每行一条操作。
输出格式每条 ask 一行答案。

输入

7
add cat 123
add dog 456
ask cat
ask fish
del dog
ask dog
ask cat

输出

123
not found
not found
123
提示

★ ask 的正确写法是 find + 判 != m.end()。⚠ 拿 m[name] 问会顺手把不存在的名字建出来,后面再 ask 它就「找到」了 0,size 也对不上。

map-ops.cpp参考答案
记住这一句

★★ m[k] 是「建 + 读」的合体,查询和删除一律走 find / erase。

用到的写法:m[k] = v; m.find(k) m.erase(k) m.count(k)

做完了去哪儿

← 上一块:priority_queue:每次拿走最大的那个 · 回 STL 专题目录 · 下一块:set:去重、查在不在、从小到大 →

⚠ 本页说的是 STL 怎么用。算法本身(Dijkstra、单调栈、状压……)在53 章正课里,而每一章末尾都会告诉你它用到了哪些 STL。