set详细使用教程
C++17 std::set 使用说明
1. 概述
std::set 是 C++ 标准库中的关联容器,用于存储唯一键的集合,元素按特定顺序自动排序。
- 头文件:
#include <set> - 底层实现:通常为红黑树(平衡二叉搜索树),但标准未强制。
- 元素唯一性:同一键只能出现一次。
- 元素有序性:按比较器
Compare排序,默认std::less<Key>。 - 元素不可修改:不能通过迭代器修改元素值,因为会破坏排序。
- 迭代器类型:双向迭代器,且为只读迭代器。
2. 模板定义
template<
class Key,
class Compare = std::less<Key>,
class Allocator = std::allocator<Key>
> class set;
Key:存储的元素类型。Compare:比较器类型,必须满足严格弱序。Allocator:分配器类型,默认std::allocator<Key>。
3. 常用成员类型
| 类型 | 说明 |
|---|---|
key_type |
Key |
value_type |
Key |
size_type |
无符号整数类型 |
difference_type |
有符号整数类型 |
key_compare |
Compare |
value_compare |
比较两个 value_type 的函数对象 |
allocator_type |
Allocator |
reference |
const value_type& |
const_reference |
const value_type& |
pointer |
const value_type* |
const_pointer |
const value_type* |
iterator |
只读双向迭代器 |
const_iterator |
只读双向迭代器 |
reverse_iterator |
反向只读迭代器 |
const_reverse_iterator |
反向只读迭代器 |
node_type |
C++17 节点句柄类型 |
insert_return_type |
C++17 插入节点句柄的返回类型 |
4. 构造与初始化
std::set<int> s1; // 空 set
std::set<int> s2 = {3, 1, 4, 1, 5}; // 初始化列表,去重并排序:1,3,4,5
std::set<int> s3(s2); // 拷贝构造
std::set<int> s4(std::move(s2)); // 移动构造
std::set<int> s5(s3.begin(), s3.end()); // 范围构造
std::set<int, std::greater<int>> s6 = {1, 2, 3}; // 自定义比较器,降序:3,2,1
C++17 支持类模板参数推导:
std::set s{1, 2, 3}; // 推导为 std::set<int>
std::set s2(s.begin(), s.end()); // 推导为 std::set<int>
5. 迭代器
std::set<int> s = {1, 2, 3};
for (auto it = s.begin(); it != s.end(); ++it) {
std::cout << *it << ' ';
}
for (int x : s) {
std::cout << x << ' ';
}
// 反向遍历
for (auto it = s.rbegin(); it != s.rend(); ++it) {
std::cout << *it << ' ';
}
begin(),end(),cbegin(),cend()rbegin(),rend(),crbegin(),crend()- 迭代器为只读,不能通过
*it = value修改元素。
6. 容量
s.empty(); // 是否为空
s.size(); // 元素个数
s.max_size(); // 最大可容纳元素数
7. 修改器
| 操作 | 说明 |
|---|---|
clear() |
清空所有元素 |
insert(value) |
插入元素,返回 pair<iterator, bool> |
insert(hint, value) |
带提示插入,返回 iterator |
insert(first, last) |
范围插入,返回 void |
insert(ilist) |
初始化列表插入,返回 void |
emplace(args...) |
就地构造,返回 pair<iterator, bool> |
emplace_hint(hint, args...) |
带提示就地构造,返回 iterator |
erase(iterator) |
删除指定元素,返回下一个迭代器 |
erase(key) |
删除键为 key 的元素,返回删除个数(0 或 1) |
erase(first, last) |
删除范围,返回下一个迭代器 |
swap(other) |
交换两个 set |
extract(key) |
C++17 提取节点,返回 node_type |
extract(iterator) |
C++17 提取节点,返回 node_type |
merge(source) |
C++17 合并另一个 set,冲突键不合并 |
insert(node_type&&) |
C++17 插入节点句柄,返回 insert_return_type |
insert(hint, node_type&&) |
C++17 带提示插入节点句柄,返回 iterator |
插入示例
std::set<int> s;
auto [it1, ok1] = s.insert(10); // C++17 结构化绑定
s.emplace(20);
s.insert({30, 40, 50}); // 初始化列表
if (auto [it, ok] = s.insert(10); !ok) {
std::cout << "10 已存在,位置:" << *it << '\n';
}
删除示例
std::set<int> s = {1, 2, 3, 4, 5};
s.erase(3); // 按 key 删除
s.erase(s.begin()); // 按迭代器删除
s.erase(s.find(4), s.end()); // 按范围删除
8. 查找
| 操作 | 说明 |
|---|---|
count(key) |
返回键为 key 的元素个数(0 或 1) |
find(key) |
返回指向 key 的迭代器,找不到返回 end() |
lower_bound(key) |
返回第一个 不小于 key 的元素 |
upper_bound(key) |
返回第一个 大于 key 的元素 |
equal_range(key) |
返回 pair<lower_bound, upper_bound> |
std::set<int> s = {1, 3, 5, 7, 9};
if (s.find(5) != s.end()) {
std::cout << "找到 5\n";
}
// C++17 if 初始化
if (auto it = s.find(5); it != s.end()) {
std::cout << "找到:" << *it << '\n';
}
auto lb = s.lower_bound(5); // 指向 5
auto ub = s.upper_bound(5); // 指向 7
注意:C++17 没有
contains(),该函数是 C++20 引入的。C++17 中判断存在性请使用find() != end()或count() > 0。
9. 观察器
auto cmp = s.key_comp(); // 返回 key 比较器
auto vcmp = s.value_comp(); // 返回 value 比较器
10. C++17 新特性
10.1 节点句柄 extract
extract 可以将元素从 set 中“提取”出来,返回一个 node_type。节点脱离容器后,可以修改其值,再重新插入。
std::set<int> s = {10, 20, 30};
auto node = s.extract(20);
if (!node.empty()) {
node.value() = 25; // 修改键值
s.insert(std::move(node)); // 重新插入
}
// s: 10, 25, 30
10.2 合并 merge
merge 将另一个 set 中的节点移动到当前 set,不会复制元素。遇到相同键时,冲突元素保留在源 set 中。
std::set<int> a = {1, 2, 3};
std::set<int> b = {3, 4, 5};
a.merge(b);
// a: 1, 2, 3, 4, 5
// b: 3 (冲突元素保留)
10.3 insert_return_type
插入节点句柄时返回 insert_return_type,包含:
struct insert_return_type {
bool inserted; // 是否插入成功
iterator position; // 插入位置或阻止插入的位置
node_type node; // 未插入的节点(失败时)
};
std::set<int> s = {1, 2, 3};
std::set<int> other = {3, 4};
auto node = other.extract(4);
auto ret = s.insert(std::move(node));
if (ret.inserted) {
std::cout << "插入成功:" << *ret.position << '\n';
} else {
std::cout << "插入失败,冲突键:" << *ret.position << '\n';
}
10.4 推导指引
C++17 为 std::set 提供了推导指引,可省略模板参数:
std::set s{1, 2, 3}; // std::set<int>
std::set s2(s.begin(), s.end()); // std::set<int>
11. 自定义比较器与自定义类型
11.1 自定义比较器
#include <functional>
std::set<int, std::greater<int>> desc = {1, 2, 3};
// 遍历顺序:3, 2, 1
使用 Lambda:
auto cmp = [](int a, int b) { return a > b; };
std::set<int, decltype(cmp)> s(cmp);
11.2 自定义类型
方式一:重载 operator<
#include <tuple>
#include <string>
struct Person {
int age;
std::string name;
bool operator<(const Person& other) const {
return std::tie(age, name) < std::tie(other.age, other.name);
}
};
std::set<Person> people;
people.insert({25, "Alice"});
people.insert({20, "Bob"});
方式二:提供比较器
struct PersonCmp {
bool operator()(const Person& a, const Person& b) const {
if (a.age != b.age) return a.age < b.age;
return a.name < b.name;
}
};
std::set<Person, PersonCmp> people;
要求:比较器必须满足严格弱序,否则行为未定义。
12. 复杂度与迭代器失效
| 操作 | 复杂度 |
|---|---|
| 插入 | O(log n) |
| 删除 | O(log n) |
| 查找 | O(log n) |
| 遍历 | O(n) |
begin / end |
O(1) |
size / empty |
O(1) |
迭代器失效规则:
insert/emplace:不会使任何迭代器失效。erase:只使指向被删除元素的迭代器失效。extract:使指向被提取元素的迭代器失效,其他迭代器不受影响。merge:指向被移动元素的迭代器仍然有效,但现在指向目标容器中的元素。swap:迭代器仍然有效,但end()迭代器可能失效。clear:使所有迭代器失效。
13. 与 std::unordered_set 对比
| 特性 | std::set |
std::unordered_set |
|---|---|---|
| 排序 | 有序 | 无序 |
| 底层结构 | 红黑树 | 哈希表 |
| 查找 | O(log n) | 平均 O(1),最坏 O(n) |
| 插入 | O(log n) | 平均 O(1),最坏 O(n) |
| 删除 | O(log n) | 平均 O(1),最坏 O(n) |
| 迭代器 | 双向 | 前向 |
| 需要 | 比较器 | 哈希函数 + 相等比较 |
| 内存 | 通常较低 | 通常较高 |
| 迭代器稳定性 | 插入不失效 | 插入可能 rehash 失效 |
14. 完整示例
#include <iostream>
#include <set>
#include <string>
#include <functional>
#include <tuple>
struct Point {
int x, y;
bool operator<(const Point& other) const {
return std::tie(x, y) < std::tie(other.x, other.y);
}
};
int main() {
// 1. 基本使用
std::set<int> s = {3, 1, 4, 1, 5};
s.insert(2);
s.emplace(6);
auto [it, ok] = s.insert(7); // C++17 结构化绑定
if (ok) {
std::cout << "插入成功: " << *it << '\n';
}
// 2. 查找
if (auto it = s.find(3); it != s.end()) { // C++17 if 初始化
std::cout << "找到: " << *it << '\n';
}
// 3. 删除
s.erase(1);
s.erase(s.begin());
// 4. 遍历
for (int x : s) {
std::cout << x << ' ';
}
std::cout << '\n';
// 5. 自定义比较器
std::set<int, std::greater<int>> desc = {1, 2, 3};
for (int x : desc) {
std::cout << x << ' ';
}
std::cout << '\n';
// 6. C++17 extract 修改元素
std::set<int> s2 = {10, 20, 30};
auto node = s2.extract(20);
node.value() = 25;
s2.insert(std::move(node));
for (int x : s2) {
std::cout << x << ' ';
}
std::cout << '\n';
// 7. C++17 merge
std::set<int> a = {1, 2, 3};
std::set<int> b = {3, 4, 5};
a.merge(b);
std::cout << "a: ";
for (int x : a) std::cout << x << ' ';
std::cout << "\nb: ";
for (int x : b) std::cout << x << ' ';
std::cout << '\n';
// 8. 自定义类型
std::set<Point> points;
points.insert({1, 2});
points.insert({1, 1});
for (const auto& p : points) {
std::cout << "(" << p.x << "," << p.y << ") ";
}
std::cout << '\n';
return 0;
}
15. 注意事项
- 不能通过迭代器修改元素:
std::set的元素是常量,*it = value会编译错误。 - 插入不会使迭代器失效,删除只使被删除元素的迭代器失效。
- C++17 没有
contains(),使用find() != end()判断存在性。 - C++17 没有
erase_if(),该函数是 C++20 引入的。 - 自定义比较器必须满足严格弱序。
- 如果比较器认为两个元素等价,则 set 中只保留一个。
- 使用 Lambda 作为比较器时,需要指定类型:
std::set<int, decltype(cmp)> s(cmp); extract后可以修改节点值,但重新插入时若键冲突会插入失败,节点句柄保留。merge不会复制元素,只移动节点;冲突元素保留在源容器中。std::set不支持operator[],因为键即值。
16. 总结
std::set 是 C++17 中非常常用的有序唯一集合容器。它基于红黑树实现,提供 O(log n) 的插入、删除和查找。C++17 引入的节点句柄(extract / merge)极大地增强了 set 的灵活性,允许在不复制元素的情况下移动节点,甚至修改原本不可变的键。使用时注意元素不可直接修改,以及自定义比较器必须满足严格弱序。
\[{\tiny 本文AI贡献大于人类}
\]
本文来自博客园,作者:wmq2012,转载请注明原文链接:https://www.cnblogs.com/wmq2012/articles/23113424

浙公网安备 33010602011771号