一、set 的核心特性
set 是关联式容器 ,底层由红黑树(平衡二叉搜索树)实现,存储的是唯一且有序的元素:
- 自动排序 :元素按值的升序自动排列(默认使用
less<T>比较函数) - 元素唯一性:不允许有重复元素,插入重复元素会失败
- 高效查找 :插入、删除、查找操作的时间复杂度均为 O(log n)
- 不支持随机访问 :不能使用下标运算符
[]按位置访问 - 迭代器是双向迭代器 :只支持
++和--操作,不支持随机访问运算 - 元素不可修改 :set 中的元素是
const类型,一旦插入不能修改,只能删除后重新插入
与 map 的关系:set 可以看作是一种特殊的 map,其中键和值是同一个元素。它们的底层实现完全相同,很多特性和操作也高度一致。
二、头文件与命名空间
cpp
#include <set> // 必须包含此头文件
using namespace std; // 或使用 std::set 前缀
三、set 的定义与初始化
set 的模板参数为 set<T, Compare = less<T>, Allocator = allocator<T>>
cpp
// 1. 空set
set<int> s1;
// 2. 拷贝构造
set<int> s2(s1);
// 3. 迭代器范围构造
int arr[] = {3, 1, 4, 1, 5, 9};
set<int> s3(arr, arr + 6); // 自动去重,s3: {1, 3, 4, 5, 9}
// 4. 列表初始化(C++11及以上)
set<int> s4 = {5, 2, 8, 1, 9, 3}; // 自动排序去重,s4: {1, 2, 3, 5, 8, 9}
set<int> s5{5, 2, 8, 1, 9, 3};
// 5. 自定义比较函数的set
set<int, greater<int>> s6; // 按值降序排列
四、元素访问
set 没有下标运算符 [] 和 at() 方法,只能通过迭代器 或查找函数访问元素:
cpp
set<int> s = {1, 2, 3, 4, 5};
// 1. 访问首尾元素
cout << *s.begin() << endl; // 输出1
cout << *s.rbegin() << endl; // 输出5
// 2. 通过find()查找元素
set<int>::iterator it = s.find(3);
if (it != s.end()) {
cout << "找到元素:" << *it << endl; // 输出3
}
// 3. 遍历访问所有元素(唯一方式)
for (int num : s) {
cout << num << " ";
}
重要特性 :set 的迭代器是
const_iterator,不能通过迭代器修改元素的值:
cpp*it = 10; // 编译错误!set元素不可修改
五、元素增删操作
(1)插入元素
cpp
set<int> s;
// 1. insert()插入单个元素
s.insert(10);
s.insert(20);
s.insert(10); // 插入重复元素,会失败
// 2. insert()插入迭代器范围的元素
int arr[] = {30, 40, 50};
s.insert(arr, arr + 3);
// 3. emplace()原地构造(C++11及以上,更高效)
s.emplace(60); // 直接在set内部构造元素,避免拷贝
// 4. 检查插入是否成功
auto result = s.insert(20);
if (result.second) {
cout << "插入成功" << endl;
} else {
cout << "插入失败,元素20已存在" << endl; // 会执行这行
}
// result.first 是指向插入位置(或已存在元素)的迭代器
上面的result.second解释:
函数insert返回的时pair<iterator,bool>result=s.insert(20)。
其中result.first代表的时插入的20,而s.second代表的时true或者是false;
(2)删除元素
cpp
set<int> s = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 1. 通过值删除
int num_erased = s.erase(5); // 删除值为5的元素,返回删除的元素个数(0或1)
cout << "删除了" << num_erased << "个元素" << endl; // 输出1
// 2. 通过迭代器删除
set<int>::iterator it = s.find(3);
if (it != s.end()) {
s.erase(it); // 删除迭代器指向的元素,返回下一个迭代器(C++11及以上)
}
// 3. 删除迭代器范围的元素
it = s.find(7);
s.erase(it, s.end()); // 删除从it到end()的所有元素
// 4. 清空所有元素
s.clear(); // s: 空
六、容量管理
set 没有 reserve() 和 capacity() 方法,因为它的内存是按需分配的:
cpp
set<int> s = {1, 2, 3, 4, 5};
// 获取元素个数
cout << s.size() << endl; // 输出5
// 判断是否为空
cout << s.empty() << endl; // 输出0(false)
// 获取set能容纳的最大元素个数
cout << s.max_size() << endl;
七、迭代器与遍历
set 的迭代器是双向迭代器,指向的是 const T 类型的元素:
cpp
set<int> s = {5, 2, 8, 1, 9, 3};
// 1. 迭代器遍历
for (set<int>::iterator it = s.begin(); it != s.end(); ++it) {
cout << *it << " ";
}
cout << endl; // 输出:1 2 3 5 8 9
// 2. 范围for循环(C++11及以上)
for (int num : s) {
cout << num << " ";
}
cout << endl;
// 3. 反向迭代器
for (set<int>::reverse_iterator rit = s.rbegin(); rit != s.rend(); ++rit) {
cout << *rit << " ";
}
cout << endl; // 输出:9 8 5 3 2 1
八、与 STL 算法结合
set 的双向迭代器不能与需要随机访问迭代器的算法配合使用。同时,set 提供了自己的成员函数版本,比通用算法更高效:
cpp
set<int> s = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 1. 查找元素(O(log n),比通用find快得多)
set<int>::iterator it = s.find(5);
if (it != s.end()) {
cout << "找到元素:" << *it << endl;
}
// 2. 统计元素出现的次数(只能返回0或1,因为元素唯一)
int count = s.count(3);
cout << "元素3出现的次数:" << count << endl; // 输出1
// 3. 查找第一个大于等于指定值的元素
it = s.lower_bound(4);
cout << "lower_bound(4): " << *it << endl; // 输出4
// 4. 查找第一个大于指定值的元素
it = s.upper_bound(4);
cout << "upper_bound(4): " << *it << endl; // 输出5
// 5. 查找等于指定值的元素范围(返回pair<iterator, iterator>)
auto range = s.equal_range(5);
for (auto it = range.first; it != range.second; ++it) {
cout << *it << " ";
}
cout << endl; // 输出:5
九、set 常见问题与注意事项
1. 迭代器失效问题
set 的迭代器失效情况非常少,与 map 完全相同:
- 插入操作:不会使任何迭代器失效
- 删除操作:只会使指向被删除元素的迭代器失效,其他迭代器仍然有效
这是 set 非常重要的特性,在频繁插入和删除元素的场景下,不需要担心迭代器失效问题。
2. 元素不可修改
set 中的元素是 const 类型,因为修改元素会破坏红黑树的排序结构。如果需要修改元素,必须先删除旧元素,再插入新元素:
cpp
set<int> s = {1, 2, 3};
// *s.find(2) = 20; // 编译错误!
// 正确做法:
s.erase(2);
s.insert(20);
3. 自定义比较函数
set 的元素按值自动排序,因此元素类型必须支持比较操作。对于自定义类型,需要提供比较函数:
cpp
// 自定义类型
struct Person {
string name;
int age;
};
// 自定义比较函数
struct ComparePerson {
bool operator()(const Person& p1, const Person& p2) const {
if (p1.name != p2.name) {
return p1.name < p2.name; // 先按姓名排序
}
return p1.age < p2.age; // 姓名相同按年龄排序
}
};
// 使用自定义比较函数的set
set<Person, ComparePerson> s;
4. multiset 容器
multiset 是 set 的变体,允许存储重复元素:
- 其他特性与 set 完全相同
count()方法可以返回大于 1 的值equal_range()方法可以返回所有等于指定值的元素范围erase(value)方法会删除所有等于指定值的元素
5. 与 unordered_set 的区别
set 和 unordered_set 都是存储唯一元素的容器,但底层实现和特性有很大不同:
- set:红黑树实现,有序,O (log n) 操作,空间开销小
- unordered_set:哈希表实现,无序,平均 O (1) 操作,空间开销大
十、set和map的区别
因为底层都是自平衡二叉搜索树(红黑树),所以它们共享以下所有特性:
表格
| 特性 | set | map |
|---|---|---|
| 底层实现 | 红黑树 | 红黑树 |
| 插入 / 删除 / 查找时间复杂度 | O(log n) | O(log n) |
| 自动排序 | 元素按值升序排列(默认less<T>) |
元素按键升序排列(默认less<Key>) |
| 元素唯一性 | 不允许重复元素 | 不允许重复键 |
| 迭代器类型 | 双向迭代器(仅支持++/--) |
双向迭代器(仅支持++/--) |
| 迭代器失效规则 | 插入不失效,删除仅失效被删元素迭代器 | 插入不失效,删除仅失效被删元素迭代器 |
| 不支持随机访问 | 不能用[]按位置访问 |
不能用[]按位置访问 |
| 自定义比较 | 支持自定义排序规则 | 支持自定义排序规则 |
二、核心区别(从根本到用法)
- 最根本区别:存储结构不同
这是所有其他区别的根源。
set:只存单个值
set 是值的集合,每个元素就是一个独立的值。
cpp
set<int> s = {1, 2, 3, 4};
// 内存中存的是:1, 2, 3, 4
map:存键值对(key-value pair)
map 是键到值的映射 ,每个元素都是一个 pair<const Key, T> 类型的对象,包含一个不可修改的键和一个对应的值。
cpp
map<int, string> m = {{1, "one"}, {2, "two"}};
// 内存中存的是:
// pair(1, "one"), pair(2, "two")
- 功能定位完全不同
set = 有序集合
-
核心作用:去重 + 排序
-
解决的问题:"这个元素是否存在?"、"所有元素按顺序排列是什么样?"
map = 有序字典 / 映射表
-
核心作用:通过键快速查找对应的值
-
解决的问题:"键为 x 的元素对应的值是什么?"、"建立 x 到 y 的对应关系"
- 元素访问方式不同
set:没有键,只能通过值或迭代器访问
-
没有
operator[]和at()方法(因为没有键可以索引) -
只能通过
find()查找元素,或者通过迭代器遍历 -
不能通过位置访问元素
cpp
set<int> s = {1, 2, 3};
// 正确:通过find查找
auto it = s.find(2);
if (it != s.end()) cout << *it << endl;
// 错误:没有[]运算符
// cout << s[2] << endl;
// 错误:没有at()方法
// cout << s.at(2) << endl;
map:必须通过键访问值
-
有
operator[]和at()方法(通过键索引) -
可以通过
find()查找键值对 -
不能通过位置访问元素
cpp
map<int, string> m = {{1, "one"}, {2, "two"}};
// 正确:通过[]访问
cout << m[1] << endl; // 输出"one"
// 正确:通过at()访问
cout << m.at(2) << endl; // 输出"two"
// 正确:通过find查找
auto it = m.find(1);
if (it != m.end()) cout << it->second << endl;
重要陷阱 :map 的
operator[]如果访问不存在的键,会自动插入一个默认值的键值对,而 set 根本没有这个问题。
- 元素可修改性不同
set:元素完全不可修改
set 中的元素是 const T 类型,一旦插入就不能修改。因为修改元素会破坏红黑树的排序结构。
cpp
set<int> s = {1, 2, 3};
auto it = s.find(2);
// *it = 20; // 编译错误!set元素不可修改
// 正确做法:删除旧元素,插入新元素
s.erase(it);
s.insert(20);
map:键不可修改,值可以修改
map 中的键是 const Key 类型,不能修改;但值是普通的 T 类型,可以任意修改。
cpp
map<int, string> m = {{1, "one"}, {2, "two"}};
auto it = m.find(1);
// it->first = 10; // 编译错误!键不可修改
it->second = "ONE"; // 正确!值可以修改
cout << m[1] << endl; // 输出"ONE"
- 迭代器指向不同
set 迭代器:指向 const T
cpp
set<int>::iterator it = s.begin();
// *it 是 const int& 类型
map 迭代器:指向 pair<const Key, T>
cpp
map<int, string>::iterator it = m.begin();
// it->first 是 const int& 类型(键)
// it->second 是 string& 类型(值)
- 特殊成员函数不同
map 有两个 set 没有的核心成员函数:
-
operator[]:通过键访问值,不存在则插入 -
at():通过键访问值,不存在则抛出异常
- 变体容器对应关系
| 基础容器 | 允许重复的变体 | 区别 |
|---|---|---|
| set | multiset | 允许存储重复元素 |
| map | multimap | 允许存储重复键 |
三、代码写法直观对比
| 操作 | set | map |
|---|---|---|
| 定义 | set<int> s; |
map<int, string> m; |
| 插入 | s.insert(10); |
m.insert({10, "ten"}); 或 m[10] = "ten"; |
| 查找 | auto it = s.find(10); |
auto it = m.find(10); |
| 访问 | cout << *it << endl; |
cout << it->first << ": " << it->second << endl; |
| 修改 | 不能修改,只能删了重插 | it->second = "TEN"; |
| 删除 | s.erase(10); 或 s.erase(it); |
m.erase(10); 或 m.erase(it); |
| 遍历 | for (int num : s) cout << num << endl; |
for (auto& p : m) cout << p.first << ": " << p.second << endl; |
四、适用场景对比
什么时候用 set?
-
需要对一组元素进行自动去重
-
需要元素保持有序
-
主要操作是判断元素是否存在
-
需要快速查找最大 / 最小值(
*s.begin()和*s.rbegin()) -
需要对元素进行集合运算(交集、并集、差集等)
典型场景:
-
统计文章中出现的不同单词
-
存储用户 ID(保证唯一)
-
实现一个有序的任务队列
什么时候用 map?
-
需要建立键到值的对应关系
-
需要通过键快速查找值
-
需要按键有序存储键值对
-
需要统计某个元素出现的次数
典型场景:
-
字典(单词到释义的映射)
-
学生成绩管理(学号到成绩的映射)
-
统计字符串中每个字符出现的次数
-
配置文件解析(配置项到值的映射)
五、终极对比表
| 特性 | set | map |
|---|---|---|
| 存储内容 | 单个值 | 键值对(pair<const Key, T>) |
| 核心功能 | 去重、排序、存在性判断 | 键值映射、快速查找 |
| 元素唯一性 | 值唯一 | 键唯一 |
| 排序依据 | 元素值 | 键 |
| operator\[\] | 无 | 有(自动插入) |
| at() | 无 | 有(抛异常) |
| 元素可修改性 | 完全不可修改 | 键不可修改,值可修改 |
| 迭代器指向 | const T | pair<const Key, T> |
| 典型用法 | set<int> s = {1,2,3}; |
map<int, string> m = {``{1,"one"}}; |
六、一句话总结
-
set 是存单个元素的有序集合,用来去重和判断存在性
-
map 是存键值对的有序字典,用来通过键快速查值