multimap = map + "键可以重复",用来表达"一对多"。 典型场景:一个作者对应多本书、一个班对应多个学生、一个词对应多个出处。它有一条必须记牢的差别:multimap 没有 operator[] ——因为"一个键对应多个值",m[k] 到底该返回哪一个?说不清,所以标准直接不给。取值靠 equal_range(一次拿到某键的所有值)或 find + 计数循环。
这是 13.2 关联式容器的最后一格。把四兄弟摆在一起就一目了然:
| 只存键 | 键 + 值 | |
|---|---|---|
| 键唯一 | set(13.2.1) |
map(13.2.3) |
| 键可重复 | multiset(13.2.2) |
multimap(本节) |
从
map到multimap的改动只有一条(允许重复),但牵动了operator[]的去留——这是"一条规则怎么影响整个接口设计"的绝佳例子。 同时它是 13.2.2 的multiset的"加值版":multiset用equal_range拿"一段相同的键",multimap用同样的手法拿"一段相同的键值对"。 顺便兑现一个上一节的遗留问题:13.2.3 里问"map怎么按值排序",答案之一就是倒过来建multimap<值, 键>——本节末尾会示范。
一、为什么 multimap 没有 operator[]
map<string, string> m;
m["张三"] = "13800000001"; // ✅ 一个键一个值,"给张三设电话"没歧义
multimap<string, string> mm;
// mm["张三"] = "13800000001"; // ❌ 编译不过!
报错是:no match for 'operator[]'。
为什么标准要这么设计:
mm 里可能是这样的:
"张三" -> "13800000001"
"张三" -> "13900000002" ← 同一个人俩电话,都合法
"张三" -> "13700000003"
那么 mm["张三"] 该给你哪一个?
第一个?最后一个?全都给?
没有"显然合理"的答案,于是标准干脆不给。
这一条其实很值得琢磨:接口能被提供出来,前提是"它的语义是明确的"。 一旦语义有歧义,宁可不给。 C++ 里同样的取舍还有很多:
vector没有push_front(那是deque的)、list没有[](它给不了随机访问)……"不给"比"给个含糊的"更负责。
那取值怎么办? 两个工具:
| 工具 | 用途 |
|---|---|
equal_range(k) |
一次拿到"所有键为 k 的元素"这一段(最常用) |
find(k) + count(k) |
先找到第一个,再数一数有几个,然后往后走 |
二、multimap 的基本用法
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
// ── ① 插入:只能用 insert / emplace(没有 [])──
multimap<string, string> books;
books.insert(make_pair("金庸", "射雕英雄传"));
books.insert(make_pair("金庸", "神雕侠侣"));
books.insert(make_pair("金庸", "天龙八部"));
books.insert(make_pair("古龙", "多情剑客无情剑"));
books.insert(make_pair("古龙", "绝代双骄"));
books.insert({"鲁迅", "朝花夕拾"}); // C++11 花括号
// ── ② 遍历:按键排好序,同键的挨在一起 ──
cout << "全部作品(按作者排序):" << endl;
for (const auto &p : books)
cout << " " << p.first << " —— " << p.second << endl;
// ── ③ 查某个作者的所有作品:equal_range ──
cout << "-- 金庸的作品 --" << endl;
auto r = books.equal_range("金庸"); // pair<iterator, iterator>
for (auto it = r.first; it != r.second; ++it)
cout << " " << it->second << endl;
// ── ④ 数一数某作者有几本 ──────────────
cout << "金庸有 " << books.count("金庸") << " 本,"
<< "古龙有 " << books.count("古龙") << " 本,"
<< "鲁迅有 " << books.count("鲁迅") << " 本" << endl;
// ── ⑤ 删除某作者的全部作品 ─────────────
books.erase("古龙"); // ★ 一次删掉键为古龙的"所有"
cout << "删掉古龙之后剩 " << books.size() << " 本" << endl;
// ── ⑥ 只删一本:用迭代器 ───────────────
auto it = books.find("金庸");
if (it != books.end()) books.erase(it); // 删掉找到的那一本
cout << "金庸还剩 " << books.count("金庸") << " 本" << endl;
return 0;
}
看到什么算成功:
全部作品(按作者排序):
古龙 —— 多情剑客无情剑
古龙 —— 绝代双骄
鲁迅 —— 朝花夕拾
金庸 —— 射雕英雄传
金庸 —— 神雕侠侣
金庸 —— 天龙八部
-- 金庸的作品 --
射雕英雄传
神雕侠侣
天龙八部
金庸有 3 本,古龙有 2 本,鲁迅有 1 本
删掉古龙之后剩 4 本
金庸还剩 2 本
四点和 map 的差异:
| 操作 | map |
multimap |
|---|---|---|
m[k] |
✅ | ❌ 没有 |
insert |
返回 pair<iterator,bool> |
返回 iterator(一定能插进去) |
erase(k) |
删 0 或 1 个 | 删掉所有键为 k 的 |
count(k) |
0 或 1 | ≥ 1 |
注意输出的排序:中文按键的字节序排("古龙" < "鲁迅" < "金庸"),跟拼音、笔画都没有关系。要在中文场景按拼音排,必须自己写比较器。
三、multimap 和 map 的选型:什么时候必须用它
| 需求 | 用什么 |
|---|---|
| "一个人一个电话" | map<string, string> |
| "一个人多个电话,要能一次列出全部" | multimap<string, string> |
| "一个人多个电话,还要能方便地增删其中一个" | map<string, vector<string>> ← 注意这个! |
| "一个词在文件里出现的所有位置" | multimap<string, int> |
| "要按'值'排序"(比如词频榜) | multimap<int, string>(把值当键) |
第 3 行值得单独说:
// 用 multimap 存"多电话",想删中间的某一个很别扭(要靠 equal_range 走)
// 用 map<string, vector<string>> 就直观多了:
map<string, vector<string>> phones;
phones["张三"].push_back("13800000001"); // ★ 一句就加一个
phones["张三"].push_back("13900000002");
phones["张三"].erase(phones["张三"].begin()); // ★ 删第一个也很自然
for (const auto &p : phones) {
cout << p.first << " 有 " << p.second.size() << " 个号码:";
for (const string &n : p.second) cout << n << " ";
cout << endl;
}
这是一条很实用的经验:"一对多"常常用
map<K, vector<V>>比multimap更好用——因为"这一组值"变成了一个真正的vector,什么操作都能做(按下标取、排序、插入中间……)。multimap的优势只在"整组一起遍历"时。 这个map<K, vector<V>>就是 13.1.1 的vector和 13.2.3 的map的嵌套——容器里装容器,这在实战里非常常见。
四、动手 Demo:把 13.2.3 的"词频榜"用 multimap 重做
任务:13.2.3 里"按词频排序"要"搬到 vector 再 sort"。这里换个思路:把"次数"直接当键,让 multimap 替你排好。
#include <iostream>
#include <map>
#include <string>
#include <sstream>
using namespace std;
int main() {
string text =
"the quick brown fox jumps over the lazy dog "
"the dog barks and the fox runs away";
// ① 先统计词频(和 13.2.3 一样)
map<string, int> cnt;
stringstream ss(text);
string w;
while (ss >> w) cnt[w]++;
// ② 倒过来:把"次数"当键,把"词"当值 → multimap 自动按次数排好
multimap<int, string> ranked;
for (const auto &p : cnt)
ranked.insert(make_pair(p.second, p.first)); // ★ 键=次数,值=词
// ③ 从后往前遍历:次数最多的在最右
cout << "-- 词频榜(从高到低)--" << endl;
auto it = ranked.rbegin(); // 反向迭代器从最大开始
int rank = 0;
for (; it != ranked.rend(); ++it) {
if (++rank > 5) break; // 只取前 5
cout << " " << rank << ". " << it->second
<< "(" << it->first << " 次)" << endl;
}
return 0;
}
看到什么算成功:
-- 词频榜(从高到低)--
1. the(5 次)
2. runs(1 次)
3. over(1 次)
...
注意这张榜的"问题":次数相同的词,顺序由"词"的字典序决定(因为是倒着遍历,所以看到的是反过来)。这说明
multimap<int,string>的排序是"先按次数、次数相同按词的字典序"。 两种做法各有场景: - "搬到vector再sort"(13.2.3):完全控制排序规则、能自由处理并列; - "倒过来建multimap"(本节):代码更短、不用写比较函数,但并列时的次序由键本身决定。 实战里前一种更常用(因为"并列怎么排"通常是有要求的),但这一种能让你更深刻地理解"关联式容器的有序性是被键决定的"。
30 秒实验
- 验证没有
operator[]:写mm["x"] = "y";→no match for 'operator[]'。 - 验证
erase(键)删一片:multimap<string,string> mm; mm.insert({"a","1"}); mm.insert({"a","2"}); mm.erase("a");→size()变成 0(两个都被删了)。要留一个:mm.erase(mm.find("a")); equal_range两端相等的情况:auto r = mm.equal_range("不存在的键");→r.first == r.second(空区间),循环体一次都不进。这就是"没有这个键"的判断写法。map<K, vector<V>>对比:把上面 Demo 换成map<int, vector<string>>(次数 → 所有该次数的词),体验一下"并列时把词收集进同一个 vector"有多方便。
常见误区
- "
multimap也能m[k]"——不能(语义有歧义,标准不给)。取值用equal_range/find。 - "
mm.erase(k)和map一样只删一个"——删掉所有键为 k 的(和 13.2.2multiset同理)。只删一个要传迭代器。 - "
find(k)拿到的那一个就够用了"——find返回的只是"这一串相同键里的某一个"(标准不保证是哪一个)。要拿到全部,必须equal_range。 - "同键元素的顺序就是我插入的顺序"——标准不保证。如果有"顺序很重要"的需求,把"序号"也放进值里,或者直接用
map<K, vector<V>>(vector保序)。 - "
multimap比map<K, vector<V>>更好"——看需求:整组一起读用multimap简洁;要"在一组里增删改排序",map<K, vector<V>>顺手得多。 - "
multimap能按值排序"——不能(它只按键有序)。但把"要排的那个东西"当键放进来就行(本节 Demo 的做法)——这是"绕开容器限制"的常用手法。
自测三问(点开对答案)
1. **为什么 `multimap` 没有 `operator[]`?**——因为**一个键对应多个值**,`m[k]` 该返回哪一个**没有明确答案**。**接口只有在语义明确时才该被提供。** 取值用 **`equal_range(k)`**(拿全部)或 **`find(k)`**(拿第一个)。 2. **想表达"一个人多个电话",`multimap- ☐ 下一节(13.3 算法)
相关笔记
- map类模板 —— 上一节:键唯一的字典
- 算法 —— 下一节:STL 的第二大件
- multiset 类模板 ——
equal_range的第一次登场 - 关联式容器 —— 四兄弟的完整对照表
- 向量类模板 ——
map<K, vector<V>>里的那个vector - 🔷 C++基础 · 索引 —— 全课路线图