set学习总结
set 学习总结
std::set 是 C++ 标准库中最常用的有序关联容器之一,本文从零到熟练梳理它的所有核心用法。
1. set 是什么
- 存储不重复的元素,每个元素唯一。
- 底层是红黑树(平衡二叉搜索树),插入、查找、删除都是 O(log n)。
- 自动按元素排序(默认升序),遍历时天然有序。
- 元素是 const 的(不能通过迭代器修改),因为修改会破坏树的有序性。
- 属于"有序关联容器",头文件
<set>。
#include <set>
set<int> s; // 空 set,int 类型
对比记忆:set 是"只存 key 的 map"(set<T> 等价于 map<T, void>),存储唯一元素并自动排序。map 存键值对,set 只存值。
2. 定义与初始化
#include <set>
#include <string>
using namespace std;
// 1. 空 set
set<int> s1;
// 2. 初始化列表
set<int> s2{3, 1, 4, 1, 5, 9}; // 自动去重且排序:{1, 3, 4, 5, 9}
// 3. 改变排序方式,降序排列
set<int, greater<int>> s3{3, 1, 4}; // {4, 3, 1}
// 4. 从其他容器构造
vector<int> v{5, 3, 1, 2, 4};
set<int> s4(v.begin(), v.end()); // {1, 2, 3, 4, 5}
// 5. 拷贝构造
set<int> s5(s2);
注意:初始化列表中重复的元素会被自动去除(如 {3, 1, 4, 1, 5} 中的两个 1 只保留一个)。
3. 核心操作一览
| 操作 | 写法 | 说明 | 复杂度 |
|---|---|---|---|
| 插入 | s.insert(x) |
元素已存在则不插入,返回 pair<iterator, bool> |
O(log n) |
| 插入 | s.emplace(args...) |
C++11,原地构造 | O(log n) |
| 查找 | s.find(x) |
返回迭代器,未找到返回 end() |
O(log n) |
| 计数 | s.count(x) |
返回 0 或 1(判断是否存在) | O(log n) |
| 判断存在 | s.contains(x) |
C++20,直接返回 bool | O(log n) |
| 删除 | s.erase(x) |
按值删除,返回删除个数 | O(log n) |
| 删除 | s.erase(it) |
按迭代器删除,返回下一个迭代器 | O(1) 均摊 |
| 二分查找 | s.lower_bound(x) |
第一个 >= x 的迭代器 | O(log n) |
| 二分查找 | s.upper_bound(x) |
第一个 > x 的迭代器 | O(log n) |
| 二分查找 | s.equal_range(x) |
返回 pair(lower_bound, upper_bound) |
O(log n) |
| 遍历 | for (auto& x : s) |
C++11 范围 for | O(n) |
| 大小 | s.size() |
元素个数 | O(1) |
| 清空 | s.clear() |
删除所有元素 | O(n) |
4. 增删查改实战
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> s;
// ===== 插入 =====
auto ret = s.insert(10);
cout << ret.second << endl; // 1,插入成功
ret = s.insert(10);
cout << ret.second << endl; // 0,已存在,插入失败
s.insert(20);
s.insert(30);
s.emplace(40); // 原地构造
// ===== 查找 =====
auto it = s.find(20);
if (it != s.end()) {
cout << "Found: " << *it << endl; // Found: 20
}
cout << s.count(30) << endl; // 1
// s.contains(30) // C++20,等价于 count != 0
// ===== 遍历(默认升序) =====
for (auto& x : s) {
cout << x << " "; // 10 20 30 40
}
cout << endl;
// ===== 降序遍历 =====
set<int, greater<int>> s2(s.begin(), s.end());
for (auto& x : s2) {
cout << x << " "; // 40 30 20 10
}
cout << endl;
// ===== 删除 =====
s.erase(20); // 按值删除,返回 1
s.erase(s.begin()); // 按迭代器删除
s.clear();
cout << s.size() << endl; // 0
return 0;
}
5. set 的"去重+排序"特性(重点)
set 最大的特点就是自动去重且排序,这在很多场景下非常方便:
#include <iostream>
#include <set>
#include <vector>
using namespace std;
int main() {
// 场景:从乱序有重复的数据中提取不重复且有序的元素
vector<int> v{5, 3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
set<int> s(v.begin(), v.end());
for (auto& x : s) {
cout << x << " "; // 1 2 3 4 5 6 9
}
cout << endl;
return 0;
}
坑:set 的元素是 const 的,不能通过迭代器修改!
set<int> s{1, 2, 3};
auto it = s.begin();
// *it = 10; // 编译错误!元素是 const
// 想修改元素:先删后插
s.erase(it);
s.insert(10); // OK
6. 排序:set 的排序由比较器决定
set 在插入时就按元素排好序,之后不能动态改。排序规则在创建时通过第二个模板参数指定:
#include <set>
#include <functional>
#include <string>
using namespace std;
// 默认:升序
set<int> s1;
// 降序
set<int, greater<int>> s2;
// 自定义比较器(函数对象)
struct Cmp {
bool operator()(const string& a, const string& b) const {
return a.size() < b.size(); // 按字符串长度排序
}
};
set<string, Cmp> s3;
s3.insert("hello");
s3.insert("hi"); // "hi" 排在 "hello" 前面(长度 2 < 5)
s3.insert("world");
// C++20:lambda 作为比较器
auto cmp = [](int a, int b) { return a > b; };
set<int, decltype(cmp)> s4(cmp);
关键限制(与 map 相同):
- 比较器必须是严格弱序。
- 创建后不能换比较器。
- 按非默认方式排序必须在声明时指定比较器。
7. 二分查找:lower_bound / upper_bound / equal_range
set 天然有序,可以直接二分查找:
set<int> s{1, 3, 5, 7, 9};
auto it1 = s.lower_bound(4); // 第一个 >= 4 的元素 -> 5
auto it2 = s.upper_bound(4); // 第一个 > 4 的元素 -> 5
auto it3 = s.lower_bound(5); // -> 5
auto it4 = s.upper_bound(5); // -> 7
// equal_range 同时拿到 [lower, upper)
auto [lo, hi] = s.equal_range(5); // 覆盖 == 5 的区间(对 set 只有一个元素)
for (auto it = lo; it != hi; ++it) {
cout << *it << endl; // 5
}
小技巧:equal_range 配合 multiset 才是真正的"批量取相同元素"场景。
8. 迭代器的特点
- set 的迭代器是双向迭代器:只支持
++/--,不支持it + n(vector 才可以)。 - 迭代器指向的元素是 const 的,不能修改。
- 删除元素只会使指向被删元素的迭代器失效,其它迭代器不受影响,所以可以边遍历边删:
set<int> s{1, 2, 3, 4, 5};
for (auto it = s.begin(); it != s.end();) {
if (*it % 2 == 0) it = s.erase(it); // 删除偶数,erase 返回下一个迭代器
else ++it;
}
// s: {1, 3, 5}
9. set 与 unordered_set、multiset 的对比
set |
unordered_set |
multiset |
|
|---|---|---|---|
| 底层 | 红黑树 | 哈希表 | 红黑树 |
| 有序 | 按元素有序 | 无序 | 按元素有序 |
| 元素重复 | 不允许 | 不允许 | 允许 |
| 复杂度 | 插入/查找/删除 O(log n) | 平均 O(1) | O(log n) |
| 迭代器 | 双向 | 无序(单向) | 双向 |
| 适用场景 | 需要有序、范围查询 | 只求查找速度 | 需要统计重复次数 |
#include <unordered_set>
unordered_set<int> us; // 查找最快,但遍历顺序不确定
#include <set>
multiset<int> ms;
ms.insert(1);
ms.insert(1); // 允许重复
ms.insert(2);
cout << ms.count(1); // 2,统计重复次数
选择建议:要排序/范围查询用 set,只追求 O(1) 查找用 unordered_set。
10. 自定义类型做元素
元素必须能"比较大小",所以自定义类型要重载 operator<(满足严格弱序):
#include <set>
#include <string>
using namespace std;
struct Student {
string name;
int score;
bool operator<(const Student& other) const { // 必须 const
return score < other.score; // 按分数排序
}
};
set<Student> s;
s.insert({"Alice", 90});
s.insert({"Bob", 85});
s.insert({"Charlie", 95});
// 遍历时按 score 升序:Bob(85) -> Alice(90) -> Charlie(95)
11. set 与 map 的关系
set<T> 本质上就是 map<T, none_type>——只存 key 不存 value:
// set 的 insert
s.insert(10);
// 等价于 map 的操作
map<int, monostate> m;
m.insert({10, monostate{}});
所以 set 的所有接口(find、count、erase、lower_bound 等)与 map 的 set 部分完全一致。
multiset 对应 multimap:允许重复元素的有序关联容器。
12. 常见坑汇总
[]不存在:set 没有operator[],查找用find/count/contains。- 元素是 const:不能通过迭代器修改元素。想"修改":先
erase旧值,再insert新值。 - 不能用会改元素的 STL 算法:
std::sort、std::remove、std::unique等会修改元素的算法不能直接用于 set(元素是 const)。set 有自己的sort吗?没有!set 的排序由比较器决定,插入时自动排序。 - 自定义类型忘了写
operator<→ 编译报错。 - 比较器不满足严格弱序 → 行为未定义。
- 遍历时修改元素:不行,编译器直接报错(元素是 const)。
- set 的迭代器是双向的,别写
it + 1。 insert不覆盖:已存在则不插入。想覆盖用erase+insert,或用map。clear不释放内存:与 vector 类似,但 set 底层是树,clear 会释放节点内存。
13. 实战场景
给定一个需求:需要存储一组唯一的数据,支持快速查找、自动排序、范围查询。
方法一:使用std::vector + std::sort + std::unique建模,但插入时去重需要 O(n) 移动元素,不适合频繁增删。
方法二:使用std::set建模,插入/查找/删除都是 O(log n),自动去重且排序,支持 lower_bound/upper_bound 范围查询。
方法三:使用std::unordered_set建模,查找 O(1) 最快,但无序、不支持范围查询。
总结:
1、需要有序且去重,选择 set
2、只需要快速查找不关心顺序,选择 unordered_set(O(1) 平均)
3、需要统计重复次数,选择 multiset
4、需要按键查找+按值排序输出,选择 map 或 set + vector 组合
5、频繁检查元素是否存在:set 的 count / contains 比 vector 的 find 快得多(O(log n) vs O(n))
6、set 的迭代器是双向的,不能 it+n 跳跃访问;vector 的迭代器是随机访问的,支持 it+n
另外红黑树不是连续内存,遍历时跳内存访问速度慢,vector 连续存储遍历快

浙公网安备 33010602011771号