multiset 和 set 的差别只有一条:允许键重复。 用法几乎完全一样(insert / find / erase / 自动升序),但有两处必然不同:① size() 会算上重复的,count(x) 能返回大于 1;② insert 不再"插不进去",所以它的返回值(iterator 而不是 pair)也不同。 另外它多了一组"处理一批相同键"的工具:equal_range(一次拿到"这段相同键"的首尾)、lower_bound / upper_bound。
上一节(13.2.1)说过:set / multiset 的差别只有"允不允许重复"。这一节就是把这句"只有"讲透——因为"允许重复"这一条会牵动好几个接口的行为(count 的返回值、insert 的返回值、erase 删几个)。
把它和 13.1.1 的 vector 对照着看:multiset 就是"能自动排序、能快速查找的 vector"——vector 里会有重复、但查找是 O(n);multiset 里也有重复、查找是 O(log n)。
这和 10.4.3 静态类成员里"计数"的思路可以互相印证:"数某个值出现几次"这件事,vector 要自己循环,multiset 用 count 一句话。
一、唯一的差别:重复怎么办
先看一眼同样的输入在两个容器里的结果:
#include <iostream>
#include <set>
using namespace std;
int main() {
int arr[] = {5, 1, 5, 3, 5, 1, 7};
set<int> s(arr, arr + 7);
multiset<int> m(arr, arr + 7);
cout << "原始数据: 5 1 5 3 5 1 7(共 7 个)" << endl;
cout << "set :";
for (int x : s) cout << x << " ";
cout << " (size=" << s.size() << ")" << endl;
cout << "multiset :";
for (int x : m) cout << x << " ";
cout << " (size=" << m.size() << ")" << endl;
return 0;
}
看到什么算成功:
原始数据: 5 1 5 3 5 1 7(共 7 个)
set :1 3 5 7 (size=4)
multiset :1 1 3 5 5 5 7 (size=7)
注意 multiset 的输出是 1 1 3 5 5 5 7——重复的元素都留下来了,而且挨在一起、仍然整体有序。这就是 multiset 的全部性格:set 的所有好处 + 不去重。
为什么重复的元素一定挨在一起:因为排序规则是"相等就算同一位置",相等的元素自然并排站着。这个性质正是下面
equal_range能成立的基础。
二、三处必然不同
| 操作 | set |
multiset |
|---|---|---|
insert(x) 返回值 |
pair<iterator, bool>(bool 表示插没插进去) |
iterator(一定能插进去,所以不需要 bool) |
count(x) 返回值 |
只有 0 或 1 | 可以 ≥ 2(真正的"数个数") |
erase(x)(传键) |
删 0 或 1 个 | 删掉所有等于 x 的元素(可能好几个!) |
size() |
去重后的个数 | 含重复的总个数 |
其余(find / 遍历 / 有序性) |
一样 | 一样 |
#include <iostream>
#include <set>
using namespace std;
int main() {
multiset<int> m = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
cout << "内容:";
for (int x : m) cout << x << " ";
cout << "(size=" << m.size() << ")" << endl;
// ── ① count:真正的"数个数"──
cout << "count(5) = " << m.count(5) << "(5 出现了几次)" << endl;
cout << "count(7) = " << m.count(7) << "(没有 7)" << endl;
// ── ② insert 返回"迭代器",不是 pair ──
multiset<int>::iterator it = m.insert(5); // ★ 不是 pair
cout << "插入 5 之后,它指向 " << *it << ",当前 count(5)=" << m.count(5) << endl;
// ── ③ erase(键):一次删掉所有 5 ────────
size_t n = m.erase(5); // ★ 返回值是"删掉了几个"
cout << "erase(5) 删掉了 " << n << " 个,现在 count(5)=" << m.count(5) << endl;
// ── ④ 只想删"其中一个"5:得用迭代器 ─────
m.insert(5); m.insert(5); m.insert(5);
cout << "再插三个 5,count(5)=" << m.count(5) << endl;
multiset<int>::iterator p = m.find(5); // find 返回"第一个" 5
if (p != m.end()) m.erase(p); // ★ 传迭代器:只删这一个
cout << "用迭代器删掉一个后,count(5)=" << m.count(5) << endl;
return 0;
}
看到什么算成功:
内容:1 1 2 3 3 4 5 5 5 6 9 (size=11)
count(5) = 3(5 出现了几次)
count(7) = 0(没有 7)
插入 5 之后,它指向 5,当前 count(5)=4
erase(5) 删掉了 4 个,现在 count(5)=0
再插三个 5,count(5)=3
用迭代器删掉一个后,count(5)=2
这张输出里藏着本节最值钱的一条经验:
erase(键)和erase(迭代器)在这里的含义差得非常远: -m.erase(5)→ 删掉所有 5(返回删了几个) -m.erase(m.find(5))→ 只删一个 5在
multiset里写错这一处,会一次性删掉好几个元素。 在set里因为最多只有一个,看不出区别——所以这个坑只有写multiset时才暴露。
三、equal_range:一次拿到"所有相同的键"
既然相等的元素必然挨在一起,那"这一段"就可以用一对迭代器表示出来:
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main() {
multiset<string> m = {"apple", "banana", "apple", "cherry",
"apple", "banana", "date"};
cout << "内容:";
for (const string &s : m) cout << s << " ";
cout << endl;
// ── ① equal_range:拿到"apple 这一段"的 [首, 尾) ──
pair<multiset<string>::iterator, multiset<string>::iterator> r = m.equal_range("apple");
cout << "所有 apple:";
for (multiset<string>::iterator it = r.first; it != r.second; ++it)
cout << *it << " ";
cout << endl;
// ── ② lower_bound / upper_bound:分别拿首和尾 ──
cout << "lower_bound(\"banana\") → " << *m.lower_bound("banana") << endl;
cout << "upper_bound(\"banana\") → " << *m.upper_bound("banana") << endl;
// ── ③ 实际用途:输出"每个词出现了几次"(有序版本)──
cout << "-- 词频(按字典序,含重复)--" << endl;
for (multiset<string>::iterator it = m.begin(); it != m.end(); ) {
pair<multiset<string>::iterator, multiset<string>::iterator> range = m.equal_range(*it);
int cnt = 0;
for (multiset<string>::iterator j = range.first; j != range.second; ++j) cnt++;
cout << " " << *it << " × " << cnt << endl;
it = range.second; // ★ 直接跳到"这一段之后"
}
return 0;
}
看到什么算成功:
内容:apple apple apple banana banana cherry date
所有 apple:apple apple apple
lower_bound("banana") → banana
upper_bound("banana") → cherry
-- 词频(按字典序,含重复)--
apple × 3
banana × 2
cherry × 1
date × 1
三个函数的分工(记住 lower / upper 的"不对称"):
| 函数 | 返回 |
|---|---|
lower_bound(x) |
第一个 >= x 的位置 |
upper_bound(x) |
第一个 > x 的位置 |
equal_range(x) |
[lower_bound(x), upper_bound(x)),即"所有等于 x 的元素"这一段 |
这个"边界不对称"的设计正是为了处理重复:
lower_bound含、upper_bound不含,于是两者的差就是等于 x 的元素个数。在set里这个差别看不出来(最多一个),在multiset里就是它存在的全部理由。 顺带说一句:lower_bound/upper_bound不是关联式容器的专利——<algorithm>里也有同名的两个函数,用在已排序的vector上(13.3.3)。这正是 13.3.3 要讲的"二分查找"的地基。
四、动手 Demo:成绩统计(multiset 的正确用法)
任务:全班分数(有重复),要自动排好、能立刻算出某个分数有几人、还要取最高最低。
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main() {
// 全班分数(含重复;multiset 自动排序)
int raw[] = {88, 95, 72, 88, 60, 95, 88, 100, 72, 55};
multiset<int> scores(raw, raw + 10);
cout << "全班 " << scores.size() << " 人,成绩(已排序):";
for (int s : scores) cout << s << " ";
cout << endl;
// ① 最高 / 最低
cout << "最低分 = " << *scores.begin() << "(第一个)" << endl;
cout << "最高分 = " << *scores.rbegin() << "(最后一个)" << endl;
// ② 每个分数有几个人(利用"相同元素挤在一起")
cout << "-- 分数分布 --" << endl;
for (multiset<int>::iterator it = scores.begin(); it != scores.end(); ) {
pair<multiset<int>::iterator, multiset<int>::iterator> r = scores.equal_range(*it);
int n = 0;
for (multiset<int>::iterator j = r.first; j != r.second; ++j) n++;
cout << " " << *it << " 分:" << n << " 人" << endl;
it = r.second; // ★ 跳到下一段
}
// ③ 查"88 分有几个"
cout << "88 分有 " << scores.count(88) << " 人" << endl;
// ④ 及格率
int pass = 0;
for (int s : scores) if (s >= 60) pass++;
cout << "及格 " << pass << " 人,及格率 "
<< (double)pass / scores.size() * 100 << "%" << endl;
// ⑤ 删掉所有不及格的
for (multiset<int>::iterator it = scores.begin(); it != scores.end(); ) {
if (*it < 60) it = scores.erase(it); // ★ erase 返回"下一个位置"
else ++it;
}
cout << "清掉不及格后剩 " << scores.size() << " 人:";
for (int s : scores) cout << s << " ";
cout << endl;
return 0;
}
看到什么算成功:
全班 10 人,成绩(已排序):55 60 72 72 88 88 88 95 95 100
最低分 = 55(第一个)
最高分 = 100(最后一个)
-- 分数分布 --
55 分:1 人
60 分:1 人
72 分:2 人
88 分:3 人
95 分:2 人
100 分:1 人
88 分有 3 人
及格 9 人,及格率 90%
清掉不及格后剩 9 人:60 72 72 88 88 88 95 95 100
这个 Demo 里最有价值的是"分数分布"那段循环:
for (it = scores.begin(); it != scores.end(); ) {
auto r = scores.equal_range(*it); // 拿到"当前这个分数"的整段
... 数一数 ...
it = r.second; // ★ 直接跳到下一段
}
如果不这么写(改成 ++it),循环次数会是 10 次(走遍每个元素);用 equal_range 跳到下一段,只要 6 次(走遍每种分数)。这是"利用容器的结构特性写出高效代码"的典型例子。
30 秒实验
erase的两种含义:写multiset<int> m = {5,5,5}; m.erase(5);看size()→ 变成 0(全删了);改成m.erase(m.find(5));→ 变成 2(只删一个)。insert返回值的类型:写auto r = m.insert(7);然后cout << r.second;→ 编译不过(multiset的insert返回的是迭代器,不是pair)。和set一起记:set要bool说"插进去没有",multiset不必。equal_range的中间差:auto r = m.equal_range(88); cout << distance(r.first, r.second);→ 正好是 88 的个数(这就是"边界不对称"的用途)。count的效率:m.count(x)在multiset里不是 O(log n),而是 O(log n + 个数)(要把这一段数一遍)。要"个数"就用count,要"知道在不在"就用find(或count也行,反正在set里一样)。
常见误区
- "
multiset和set差不多,随便用哪个"——差在"删"上:ms.erase(x)一次删光所有 x。很多人从set换到multiset时栽在这里——本来只想删一个,结果全没了。要删一个:ms.erase(ms.find(x))。 - "
multiset的insert也能if (m.insert(x).second)"——编译不过(返回的是iterator)。这一条是"两兄弟接口不同"的具体体现。 - "
multiset里的元素可以改"——和set一样不能改(元素仍是const,因为它是排序依据)。要改就删了再插。 - "
count在multiset里很快"——它要把相等的那一段数一遍,所以复杂度是 O(log n + 该值的个数)。数一个出现五万次的键,就是五万次。 - "
multiset也能排序"——它本来就是有序的,不需要排序。也不能用std::sort(迭代器只有双向,13.4.4)。 - "
multiset适合当"购物车"(能加同名商品)"——思路可以,但通常map<string, int>(商品名 → 数量)更合适(13.2.3)。因为"数量"这一维用map的值来记,比存一堆重复的键更省。 - "
multimap和multiset是同一件事"——不是:multiset只存键,multimap存"键 + 值"(13.2.4)。
自测三问(点开对答案)
1. **`set` 和 `multiset` 的差别是什么?会牵动哪些接口?**——差别只有一条:**允许键重复**。牵动四处:**① `insert` 返回 `iterator`(不再是 `pair- ☐ 下一节(13.2.3 map类模板)