🔷 第十三章 STL
📄 一句话概括

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 秒实验

  1. 验证没有 operator[]:写 mm["x"] = "y"; → no match for 'operator[]'。
  2. 验证 erase(键) 删一片:multimap<string,string> mm; mm.insert({"a","1"}); mm.insert({"a","2"}); mm.erase("a"); → size() 变成 0(两个都被删了)。要留一个:mm.erase(mm.find("a"));
  3. equal_range 两端相等的情况:auto r = mm.equal_range("不存在的键"); → r.first == r.second(空区间),循环体一次都不进。这就是"没有这个键"的判断写法。
  4. map<K, vector<V>> 对比:把上面 Demo 换成 map<int, vector<string>>(次数 → 所有该次数的词),体验一下"并列时把词收集进同一个 vector"有多方便。

常见误区

  1. "multimap 也能 m[k]"——不能(语义有歧义,标准不给)。取值用 equal_range / find。
  2. "mm.erase(k) 和 map 一样只删一个"——删掉所有键为 k 的(和 13.2.2 multiset 同理)。只删一个要传迭代器。
  3. "find(k) 拿到的那一个就够用了"——find 返回的只是"这一串相同键里的某一个"(标准不保证是哪一个)。要拿到全部,必须 equal_range。
  4. "同键元素的顺序就是我插入的顺序"——标准不保证。如果有"顺序很重要"的需求,把"序号"也放进值里,或者直接用 map<K, vector<V>>(vector 保序)。
  5. "multimap 比 map<K, vector<V>> 更好"——看需求:整组一起读用 multimap 简洁;要"在一组里增删改排序",map<K, vector<V>> 顺手得多。
  6. "multimap 能按值排序"——不能(它只按键有序)。但把"要排的那个东西"当键放进来就行(本节 Demo 的做法)——这是"绕开容器限制"的常用手法。
自测三问(点开对答案) 1. **为什么 `multimap` 没有 `operator[]`?**——因为**一个键对应多个值**,`m[k]` 该返回哪一个**没有明确答案**。**接口只有在语义明确时才该被提供。** 取值用 **`equal_range(k)`**(拿全部)或 **`find(k)`**(拿第一个)。 2. **想表达"一个人多个电话",`multimap` 和 `map>` 哪个好?**——**看你要干什么**:**只要"整组读出来"** → `multimap` 简洁;**要"在一组里增删改、按下标取、排序"** → **`map>` 顺手得多**(因为那一组变成了真正的 `vector`)。**实战里后者更常见。** 3. **13.2.3 说"`map` 不能按值排序",用 `multimap` 怎么绕开?**——**把"要排序的那个东西"当成键**:比如统计完词频后,建 `multimap`(**次数当键、词当值**),于是**它自动按次数排好了**。**代价**:**并列时的次序由值本身决定**,不能自定义;**要完全控制排序规则,还是"复制到 `vector` 再 `sort`"更灵活。**

相关笔记