set与multiset集合
困难42集合大作战:set 和 multiset 的自动排序与重复管理
1. 从生活中的例子引入
想象一下,你们班上要举办一场运动会,老师需要统计所有报名参加“50米跑”的同学。为了避免重复统计,老师会用一个 “名单” 来记录——每个同学的名字只能出现一次。如果小明已经报了名,他再报名时老师就会说:“你已经在名单上了,不用重复登记。”这个名单就像是 C++ 里的 set(集合):里面的元素是唯一的,而且老师会按拼音顺序把名字排好,方便查找。
再比如,你去超市买零食,列了一个 “购物清单”:薯片 2 包、巧克力 3 块、可乐 2 瓶。清单上同一个商品可以出现多次(因为你要买多份)。而且你可能把清单按商品名字的顺序写好,方便在货架上快速找到。这个“可重复、有序”的清单,就像是 C++ 里的 multiset(多重集合)。
生活中很多数据都像这样:既希望自动排序,又允许或者不允许重复。计算机里也经常需要处理这类数据,比如保存一组学号(不能重复)、统计文章里每个单词出现的次数(可重复)等。Set 和 multiset 就是专门帮我们做这件事的工具。
另外,你还能想到哪些场景?比如:
- 考试分数统计:老师想把全班成绩排序,但同分数可以出现多次(用 multiset)。
- 在线游戏匹配:系统记录所有玩家的 ID,每个 ID 只能出现一次(用 set)。
- 图书馆借书记录:每本书的索书号是唯一的,但不同读者可以借同一本书的不同副本(类似 multiset 的“相同值”概念)。
2. 讲解STL的原理和使用方法
2.1 什么是 set 和 multiset?
- set:一个有序的容器,内部元素不重复。当你插入一个元素时,如果它已经存在,插入不会成功。set 内部通常用红黑树(红黑树是一种平衡二叉搜索树)实现,所以插入、删除、查找的平均时间复杂度都是 O(log n),非常快。这比在无序列表里逐个寻找快得多(列表查找是 O(n)),尤其当数据量很大时(比如 10 万个元素),log₂(100,000) ≈ 17,而线性查找平均要 5 万次。
- multiset:与 set 类似,也是有序的,但允许元素重复。你可以插入多个相同的值,multiset 会为每个值保留多个副本。它的内部同样是红黑树,只是不强制唯一性。
2.2 头文件和命名空间
在 C++ 中使用 set 和 multiset 需要包含头文件 <set>,并使用 std:: 命名空间(或者用 using namespace std;)。
#include <set>
using namespace std;
2.3 常用成员函数详解
(1) 插入元素
insert(value):插入一个元素。对 set 来说,如果 value 已存在,则插入失败;对 multiset 来说,总是成功。返回一个pair<iterator, bool>,其中 iterator 指向新插入的位置(如果插入失败则指向已有元素的位置),bool 表示是否插入成功(仅在 set 中有意义)。- 也可以一次插入多个元素,用初始化列表:
s.insert({1,2,3}); - 还可以用
emplace(value)直接在容器中构造元素,避免拷贝,效率更高。
生活例子:想象你有一张“午饭菜单”的 set(不能重复点同样的菜)。你点“红烧肉”,成功;再点“红烧肉”,服务器告诉你“这道菜已经点过了”。而 multiset 就像超市购物车,你可以往里面放三包薯片,每放一次都记录一次。
(2) 查找元素
find(value):返回指向该元素的迭代器;如果没找到,返回end()。注意:在 multiset 中,如果存在多个相同值,find返回的迭代器指向其中第一个(通常是最先插入的或排序中的第一个)。count(value):返回元素值为 value 的个数。set 中只能是 0 或 1;multiset 中可以 >= 1。注意:count在 multiset 中也是 O(log n) 时间复杂度(加上重复个数),但实际实现中它会遍历所有等于 value 的节点,复杂度依然是 O(log n + 重复个数)。lower_bound(value)和upper_bound(value):用于范围查找,返回迭代器。lower_bound:第一个不小于 value 的元素位置(即 >= value 的第一个)。upper_bound:第一个大于 value 的元素位置。
equal_range(value):返回一个 pair,其 first 和 second 分别为lower_bound和upper_bound,可用于遍历所有等于 value 的元素(multiset 中非常有用)。
生活例子:考试结束后,老师用 set 记录所有及格的学号(唯一)。学号 1024 及格了,老师用 find 找到他;学号 2048 没及格,找不到。对于 multiset,统计学号 1024 出现的次数(比如他的分数重复出现?不,通常学号唯一,但多门课成绩可以用 multiset 记录分数)。
(3) 删除元素
erase(value):删除所有等于 value 的元素,返回被删除的个数(set 中只能是 0 或 1,multiset 中可以是任意数量)。erase(iterator):删除迭代器指向的单个元素。注意:使用迭代器删除后,该迭代器会失效,不能再使用。erase(first, last):删除迭代器区间 [first, last) 内的所有元素。clear():清空所有元素。
新手容易犯的错误:在 multiset 中,想只删除一个等于 value 的元素,却写了 erase(value),结果删除了全部。正确做法是:先用 find 得到迭代器,然后 erase(it)。另一个错误是:在循环中删除元素时,直接 erase(it++) 或 erase(it) 后没有更新迭代器。标准做法是:it = s.erase(it)(C++11 起 erase 返回下一个元素的迭代器)。
(4) 其他访问
size():返回元素个数。对于 multiset,包含重复的计数。empty():判断是否为空。begin()和end():返回首迭代器和尾后迭代器,用于遍历。
2.4 遍历
set 和 multiset 都是有序的,默认按元素值的升序排列。可以通过迭代器(或 C++11 范围 for 循环)直接遍历。
for (auto it = s.begin(); it != s.end(); ++it) {
cout << *it << " ";
}
// 或者
for (int x : s) cout << x << " ";
注意:遍历时不要修改元素(因为会破坏排序结构)。但可以删除当前迭代器指向的元素(但要小心迭代器失效)。
2.5 自定义排序
默认使用 std::less<T> 进行升序。如果希望降序,或按其他规则排序,可以在定义时传入比较器。例如:
set<int, greater<int>> s; // 降序
对于自定义类型(比如 Student),需要重载 < 运算符,或者提供函数对象。例如:
struct Student {
string name;
int score;
bool operator<(const Student& other) const {
return score > other.score; // 按分数降序排列
}
};
set<Student> s;
这部分会在后续文章中详细讲解。
3. 生活中的综合示例:比赛选手管理
假设你是一个小比赛的主办方,需要管理报名选手(每个选手只能报名一次)以及选手的得分(多次得分可以记录)。我们使用 set 管理选手 ID,用 multiset 管理得分。
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main() {
// ===== 选手管理(set)=====
cout << "=== 选手报名管理(set)===" << endl;
set<string> players; // 选手名字唯一
players.insert("Alice");
players.insert("Bob");
players.insert("Charlie");
// Alice 尝试再次报名
auto result = players.insert("Alice");
if (result.second) {
cout << "Alice 报名成功" << endl;
} else {
cout << "Alice 已经报名过了,不能重复报名" << endl;
}
// 打印所有选手(按名字字母序)
cout << "已报名选手:";
for (const string& name : players) cout << name << " ";
cout << endl;
cout << "选手人数:" << players.size() << endl;
// Bob 退赛
players.erase("Bob");
cout << "Bob 退赛后剩余选手:";
for (const string& name : players) cout << name << " ";
cout << endl << endl;
// ===== 得分管理(multiset)=====
cout << "=== 得分记录(multiset)===" << endl;
multiset<int> scores; // 得分可以有重复
// Alice 三次得分
scores.insert(85);
scores.insert(92);
scores.insert(88);
// Charlie 两次得分
scores.insert(90);
scores.insert(88);
cout << "所有得分(升序):";
for (int s : scores) cout << s << " ";
cout << endl;
// 统计某个分数出现次数
cout << "88 分出现了 " << scores.count(88) << " 次" << endl;
// 查找最低分和最高分
cout << "最低分:" << *scores.begin() << endl;
cout << "最高分:" << *scores.rbegin() << endl; // rbegin() 是反向迭代器
// 删除所有 88 分(比如裁判发现这个判分有误)
int erased = scores.erase(88);
cout << "删除了 " << erased << " 个 88 分,剩余得分:";
for (int s : scores) cout << s << " ";
cout << endl;
return 0;
}
输出结果:
=== 选手报名管理(set)===
Alice 已经报名过了,不能重复报名
已报名选手:Alice Bob Charlie
选手人数:3
Bob 退赛后剩余选手:Alice Charlie
=== 得分记录(multiset)===
所有得分(升序):85 88 88 90 92
88 分出现了 2 次
最低分:85
最高分:92
删除了 2 个 88 分,剩余得分:85 90 92
4. Python 等价功能的代码实现(带详细注释)
Python 内置的 set 和 frozenset 是无序的、不重复的集合,不保证元素顺序。但是我们可以用 list 或 collections 模块模拟有序且允许重复的集合。注意,Python 官方的 set 是基于哈希表实现的,不是红黑树,所以它是无序的。但 Python 3.7+ 中 dict 有序,我们可以利用 dict 的键来模拟有序集合。不过对于教育和竞赛,我们通常用 list 手动维护有序性,或用 bisect 模块维护升序列表。
更常用的做法是使用 collections.Counter 来统计次数(类似 multiset 的计数能力),但输出不是自动排序的。如果希望自动排序,可以结合 sorted() 或 SortedContainers 第三方库(非标准库)。
下面给出一个模拟 set 和 multiset 的简单实现(基于列表,手动保证唯一性和顺序),方便理解。
# Python 没有直接的有序唯一容器,但我们可以用 list + 二分查找实现
import bisect
class SetLike:
"""类似 set,元素唯一且有序(升序)"""
def __init__(self):
self._data = []
def insert(self, value):
# 检查是否已存在
pos = bisect.bisect_left(self._data, value)
if pos == len(self._data) or self._data[pos] != value:
self._data.insert(pos, value)
return True # 插入成功
return False # 已存在
def erase(self, value):
pos = bisect.bisect_left(self._data, value)
if pos < len(self._data) and self._data[pos] == value:
self._data.pop(pos)
return True
return False
def count(self, value):
# 使用二分查找
left = bisect.bisect_left(self._data, value)
right = bisect.bisect_right(self._data, value)
return right - left
def find(self, value):
pos = bisect.bisect_left(self._data, value)
if pos < len(self._data) and self._data[pos] == value:
return pos
return -1
def __contains__(self, value):
return self.count(value) > 0
def __repr__(self):
return str(self._data)
class MultiSetLike:
"""类似 multiset,允许重复且有序(升序)"""
def __init__(self):
self._data = []
def insert(self, value):
# 插入保持有序
bisect.insort(self._data, value)
def erase(self, value, all_=False):
"""删除一个或所有等于 value 的元素"""
if all_:
# 删除所有
left = bisect.bisect_left(self._data, value)
right = bisect.bisect_right(self._data, value)
del self._data[left:right]
return right - left
else:
# 只删除第一个
pos = bisect.bisect_left(self._data, value)
if pos < len(self._data) and self._data[pos] == value:
self._data.pop(pos)
return 1
return 0
def count(self, value):
left = bisect.bisect_left(self._data, value)
right = bisect.bisect_right(self._data, value)
return right - left
def __repr__(self):
return str(self._data)
# 使用示例
if __name__ == "__main__":
print("=== Python 模拟 set ===")
s = SetLike()
s.insert(10)
s.insert(5)
s.insert(15)
s.insert(5)
print("当前集合:", s) # [5, 10, 15]
print("是否包含 10:", 10 in s) # True
print("5 出现次数:", s.count(5)) # 1
s.erase(5)
print("删除 5 后:", s) # [10, 15]
print("\n=== Python 模拟 multiset ===")
ms = MultiSetLike()
ms.insert(10)
ms.insert(5)
ms.insert(5)
ms.insert(15)
ms.insert(5)
print("当前多重集合:", ms) # [5, 5, 5, 10, 15]
print("5 出现次数:", ms.count(5)) # 3
ms.erase(5, all_=False) # 删除一个5
print("删除一个5后:", ms) # [5, 5, 10, 15]
ms.erase(5, all_=True) # 删除所有5
print("删除所有5后:", ms) # [10, 15]
注意:上述 Python 代码仅仅是教学模拟,实际竞赛中可以直接使用 list + sort,但效率不高(插入是 O(n))。真正高效的实现需要用到 bisect 或 SortedContainers。对于信息学竞赛,Python 选手可以使用 collections.Counter 统计次数,配合 sorted() 排序输出。
更实际的做法:如果只需要自动排序和统计次数,可以直接用 list 并每次插入后排序(但要考虑效率),或者用 heapq 维护堆(但不是有序列表)。以下是一种常见做法:把所有元素放进 list,排序后输出。
# 更实用的 Python 做法:用 list + sorted()
data = [10, 5, 15, 5, 5]
data.sort() # 升序排序
print("排序后的列表(模拟 multiset):", data)
# 统计
from collections import Counter
cnt = Counter(data)
print("5 出现的次数:", cnt[5])
5. 总结要点和注意事项
- set 与 multiset 的核心区别:是否允许重复元素。
- 内部结构:基于红黑树,插入、查找、删除都是 O(log n) 级别,非常高效。
- 自动排序:默认升序(
<比较),可以通过比较器自定义排序规则。 - 成员函数使用:
insert、find、count、erase、lower_bound、upper_bound、equal_range等。 - 遍历:使用迭代器或范围 for 循环,得到有序序列。
- 注意事项:
- set 的
insert返回pair,可用于判断是否插入成功。 - multiset 的
erase(value)会删除所有等于 value 的元素,如果只想删除一个,应该用erase(iterator)。 find和count的时间复杂度都是 O(log n),但count在 multiset 中可能遍历更多元素(实际上依然 O(log n))。- 自定义类型需要重载
<运算符,或者提供比较器。
- set 的
- 常见错误:
- 误以为
erase(value)只删除一个,实际删除了全部。 - 在循环中删除元素后继续使用失效的迭代器。解决方法:使用
it = s.erase(it)(C++11 后 erase 返回下一个迭代器)。 - 试图修改 set 中的元素(比如通过解引用迭代器赋值),这会导致容器内部排序混乱。
- 忘记包含头文件
<set>。
- 误以为
- Python 对比:Python 标准库没有直接对应有序集合的容器,但可以用
bisect模块手动模拟;更实际的方法是使用set(无序唯一)和Counter(可重复无序)。如果要保持顺序,建议用list + sort或第三方库sortedcontainers。
6. 相关指引
掌握了 set 和 multiset,你就能轻松处理“自动排序、快速查找”的数据了。接下来你可以学习:
- map 和 multimap:它们是“键-值”对的有序容器,像一本自动排序的字典,可以通过键快速查找值。
- unordered_set 和 unordered_multiset:它们基于哈希表,查找更快(平均 O(1)),但顺序是乱的,适合只关心“是否存在”而不关心顺序的场景。
- 迭代器失效问题:了解在遍历过程中删除/插入元素时如何安全地操作。
- 自定义比较器:深入支持复杂类型的排序。
下一篇文章,我们将学习 map 与 multimap 映射,它们像是“有自动排序的字典”。
例题精讲
关于C++中set容器的描述,下列哪项是正确的?
C++中,multiset容器内的元素默认按升序排列,且允许重复元素存在。
以下C++代码使用set存储几个整数,并输出排序后的结果。请补全代码。
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> s;
s.___(3);
s.insert(1);
s.insert(2);
for (auto x : s) {
cout << x << " ";
}
return 0;
}对于multiset<int> ms = {5, 1, 3, 1, 2}; 执行操作 ms.erase(1); 后,ms中元素1的个数变为多少?
Python中可以用list和sort来模拟multiset的自动排序和可重复特性。以下代码实现插入若干数字并打印升序结果,请补全。
nums = []
nums.append(3)
nums.append(1)
nums.append(2)
nums.append(1)
nums.___()
print(nums)