前言:
set 和 map 是处理“集合”和“字典”需求时的首选工具。它们能在 O(log n) 的时间内完成查找、插入和删除,而且容器内的元素始终有序,这是普通数组或链表做不到的。
1. 关联容器概述
C++ 标准库提供了两大类关联容器:
| 容器 | 特点 | 底层实现 |
|---|---|---|
set | 存放唯一键,按键排序 | 红黑树 |
multiset | 存放可重复键,按键排序 | 红黑树 |
map | 存放键-值对,键唯一,按键排序 | 红黑树 |
multimap | 存放键-值对,键可重复,按键排序 | 红黑树 |
2. 头文件与基本声明
使用它们之前,需要包含对应的头文件:
#include <set> // set 和 multiset
#include <map> // map 和 multimap
声明一个 set 和 map:
std::set<int> s; // 存放 int 的集合
std::set<std::string> names; // 存放字符串的集合
std::map<std::string, int> ages; // 键为 string,值为 int 的字典
std::map<int, std::string> idToName; // 键为 int,值为 string
3. set 的基本操作
set 可以理解为自动排序且元素唯一的集合。添加相同元素时,后续的插入会被忽略。
3.1 插入元素
std::set<int> s;
s.insert(30);
s.insert(10);
s.insert(20);
s.insert(10); // 重复的 10,会被忽略
// 此时 s 内部顺序为:10 20 30
insert 返回一个 pair<iterator, bool>,其中 second 表示插入是否成功:
auto result = s.insert(10);
if (!result.second) {
std::cout << "10 已经存在,插入失败\n";
}
也可用 emplace 原地构造元素,更高效:
s.emplace(40); // 直接构造元素
3.2 查找元素
if (s.find(20) != s.end()) {
std::cout << "找到了 20\n";
}
// 也支持 count,返回 0 或 1
if (s.count(30)) {
std::cout << "30 存在\n";
}
find 返回迭代器,指向找到的元素;若未找到则返回 end()。
3.3 删除元素
s.erase(20); // 删除值为 20 的元素
s.erase(s.begin()); // 删除迭代器指向的元素
s.erase(s.begin(), s.find(40)); // 删除一个范围 [first, last)
s.clear(); // 清空
3.4 遍历 set
由于 set 是排序的,遍历时会按照升序输出(默认 std::less)。
for (int x : s) {
std::cout << x << " ";
}
// 或使用迭代器
for (auto it = s.begin(); it != s.end(); ++it) {
std::cout << *it << " ";
}
需要注意的是,set 的迭代器是常量双向迭代器,不能通过迭代器修改元素的值(否则会破坏有序性)。
4. map 的基本操作
map 是一个键-值对的集合,每个键都是唯一的。内部也是按键排序的。元素的类型是 std::pair<const Key, T>。
4.1 插入元素
最常用的是使用 insert 或 emplace,或直接用 [] 运算符。
std::map<std::string, int> ages;
// 使用 insert 插入 pair
ages.insert(std::make_pair("Alice", 25));
ages.insert({"Bob", 30}); // C++11 初始化列表
// 使用 emplace 原地构造
ages.emplace("Charlie", 28);
// 使用 [] 操作符(如果键不存在则自动插入,值默认构造)
ages["David"] = 35; // 插入 "David":35
ages["Alice"] = 26; // 修改已存在键的值
重要区别:insert 当键已存在时不会覆盖旧值(插入失败),而 [] 总会让键对应的值变成新值(如果不存在就先插入再赋值)。需要根据意图选用。
4.2 访问元素
std::cout << ages["Alice"] << "\n"; // 若不存在会插入,不安全
std::cout << ages.at("Bob") << "\n"; // 若不存在抛 std::out_of_range 异常
// 安全访问:先 find
auto it = ages.find("Charlie");
if (it != ages.end()) {
std::cout << "Charlie: " << it->second << "\n";
}
遍历 map 时,每个元素是一个 pair,通过 .first 访问键,.second 访问值。
for (const auto& p : ages) {
std::cout << p.first << " : " << p.second << "\n";
}
// 使用结构化绑定
for (const auto& [name, age] : ages) {
std::cout << name << " is " << age << " years old.\n";
}
4.3 删除元素
与 set 类似:
ages.erase("Bob"); // 按键删除
auto it = ages.find("Charlie");
if (it != ages.end()) ages.erase(it); // 按迭代器删除
5. 自定义排序规则
set 和 map 默认使用 std::less<Key>,即升序排列。可以通过第二个模板参数指定自定义比较器。
例如,想要一个降序的 set:
std::set<int, std::greater<int>> descSet = {10, 30, 20};
// 遍历输出:30 20 10
对于自定义类型,需要提供比较函数对象或 lambda。
struct Person {
std::string name;
int age;
};
// 按年龄升序排列
struct CmpByAge {
bool operator()(const Person& a, const Person& b) const {
return a.age < b.age;
}
};
std::set<Person, CmpByAge> people;
people.insert({"Alice", 30});
people.insert({"Bob", 25});
// 内部顺序:Bob(25) 在 Alice(30) 之前
// 对于 map,比较的是键,所以只需指定键的比较
std::map<Person, int, CmpByAge> personMap;
注意:比较器必须提供一个严格弱序(strict weak ordering),且不可在运行时改变排序规则(它是类型的一部分)。
6. multiset 和 multimap
当需要允许重复键时,就使用 multiset 和 multimap。它们的接口与 set/map 几乎相同,区别在于:
-
insert永远成功(插入重复键时插入一个新元素)。 -
erase如果按值删除,会删除所有该值的元素;如果按迭代器,则只删一个。 -
count可以返回大于 1 的值。 -
find返回指向第一个匹配元素的迭代器(可以使用equal_range获取一个范围)。
示例:
std::multiset<int> ms = {1, 2, 2, 3};
ms.insert(2); // 现在有两个 2?
std::cout << ms.count(2) << "\n"; // 输出 3
auto range = ms.equal_range(2); // pair<iterator, iterator>
for (auto it = range.first; it != range.second; ++it) {
std::cout << *it << " "; // 输出所有的 2
}
multimap 类似,但没有 [] 操作符(因为键不唯一,无法确定修改哪个值)。
7. set/map 与 vector/list 的选择
| 需求 | 推荐容器 |
|---|---|
| 需要快速按位置随机访问 | vector / deque |
| 频繁在头部/中间插入删除,不需要排序 | list |
| 需要自动去重 + 排序 | set |
| 需要键值映射 + 按键排序 | map |
| 仅需判断存在性,且希望 O(log n) | set |
| 需要 O(1) 平均查找,不关心顺序 | unordered_set / unordered_map |
一个常见的错误:不要仅仅为了“能快速查找”就直接用 set 或 map。如果数据不需要排序,且对性能要求极高,可以使用无序版本(哈希表)。有序版本的优势在于:可以按顺序遍历、支持范围查询(如 lower_bound/upper_bound)。
8. 有用的成员函数(高级)
set 和 map 还提供了一些实用的二分查找函数:
-
lower_bound(key):返回第一个 不小于 key 的元素的迭代器。 -
upper_bound(key):返回第一个 大于 key 的元素的迭代器。 -
equal_range(key):返回pair,等价于make_pair(lower_bound, upper_bound)。
这些在实现区间查询时非常高效,复杂度为 O(log n)
9. 性能提示与注意事项
-
内存开销:
set和map的每个节点除了存储数据,还要维护三个指针(左孩子、右孩子、父节点)以及颜色信息。相比vector,它们的内存占用较大且缓存不友好。 -
迭代器稳定性:插入和删除元素不会使其他元素的迭代器失效(被删除的那个除外),这点与
list类似。 -
元素不可变:在
set中存储的元素本身是键,不能修改(因为改了可能破坏排序)。如果确实需要修改,只能先删除再插入。 -
避免无意识的插入:使用
[]访问map时,如果键不存在会悄悄插入一个默认值,可能导致意料之外的内存占用。如果只是查找,请使用find或at。 -
比较函数必须一致:在 C++ 中,若比较规则不满足严格弱序(如写成
a <= b),行为是未定义的。
10. 小结
-
set是有序、去重的集合,map是有序、键唯一的字典。 -
底层基于红黑树,所有操作 O(log n)。
-
支持
insert、emplace、find、erase、count、lower_bound等。 -
遍历时自动按排序规则输出,默认升序,可自定义比较器。
-
若需要重复键,使用
multiset/multimap。 -
若无序但需要平均 O(1) 查找,使用
unordered_set/unordered_map。 -
多用
find和at而不是盲目用[],避免意外插入。
1220



被折叠的 条评论
为什么被折叠?



