C++ set 容器:有序唯一元素集合的深度解析与实战
在 C++ 标准库中,std::set 是一种基于红黑树实现的有序容器,它专门用于存储唯一且已排序的元素。与序列式容器(如 vector、list)不同,set 不允许元素重复,且会自动按特定规则排序,非常适合需要快速查找、去重和有序遍历的场景。本文将从底层实现、核心操作到高级应用,全面解析 std::set 的特性与使用技巧。
一、set 的本质与核心特性
1.1 底层实现与核心特征
std::set 属于关联容器(Associative Containers),其核心特性如下:
- 存储单一类型元素(而非键值对),元素本身即作为排序的依据
- 元素唯一(不允许重复,若需重复元素可使用
std::multiset) - 元素按比较规则自动排序(默认按
std::less<T>升序) - 底层基于红黑树(自平衡二叉搜索树)实现,保证插入、删除和查找的平均时间复杂度为 O (log n)
cpp
运行
#include <iostream>
#include <set>
using namespace std;int main() {// 定义一个存储 int 类型的 setset<int> s;// 插入元素(自动去重并排序)s.insert(3);s.insert(1);s.insert(2);s.insert(2); // 重复元素,插入失败// 遍历输出(自动按升序排列)for (int num : s) {cout << num << " ";}// 输出:1 2 3return 0;
}
1.2 与其他容器的区别
| 容器类型 | 底层结构 | 元素有序性 | 元素唯一性 | 查找复杂度 | 适用场景 |
|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 唯一 | O(log n) | 有序去重、快速查找 |
std::multiset | 红黑树 | 有序 | 可重复 | O(log n) | 有序允许重复、分组统计 |
std::unordered_set | 哈希表 | 无序 | 唯一 | 平均 O (1) | 无序去重、追求极致查找效率 |
std::vector | 动态数组 | 无序(需手动排序) | 可重复 | O(n) | 随机访问、频繁修改 |
二、set 的基本操作
2.1 初始化与构造
std::set 支持多种初始化方式,满足不同场景需求:
cpp
运行
#include <set>
#include <vector>
using namespace std;int main() {// 1. 默认构造set<int> s1;// 2. 列表初始化(C++11)set<int> s2 = {3, 1, 4, 1, 5}; // 自动去重排序为 {1,3,4,5}// 3. 范围构造(从其他容器复制元素)vector<int> vec = {2, 7, 1, 8};set<int> s3(vec.begin(), vec.end()); // {1,2,7,8}// 4. 复制构造set<int> s4(s2); // 复制 s2 的元素// 5. 移动构造(C++11)set<int> s5(std::move(s4)); // s4 变为空// 6. 自定义比较器构造(按降序排列)set<int, greater<int>> s6 = {3, 1, 4}; // {4,3,1}return 0;
}
2.2 插入元素
std::set 提供 insert() 方法插入元素,插入重复元素时会自动忽略:
cpp
运行
#include <set>
#include <iostream>
using namespace std;int main() {set<int> s;// 方式1:插入单个元素s.insert(1);s.insert(2);// 方式2:插入多个元素(C++11 初始化列表)s.insert({3, 4, 5});// 方式3:插入范围int arr[] = {6, 7};s.insert(arr, arr + 2);// 检查插入结果(返回 pair<迭代器, bool>,bool 表示是否插入成功)auto res = s.insert(2); // 插入重复元素if (res.second) {cout << "插入成功,元素为:" << *res.first << endl;} else {cout << "插入失败(元素已存在),已有元素为:" << *res.first << endl;}// 输出:插入失败(元素已存在),已有元素为:2return 0;
}
注意:insert() 的返回值是 pair<iterator, bool>,其中 first 是指向元素的迭代器(无论是否插入成功),second 表示插入是否成功。
2.3 查找与访问元素
std::set 不支持通过下标访问(无 operator[]),需通过迭代器或查找方法访问元素:
cpp
运行
#include <set>
#include <iostream>
using namespace std;int main() {set<int> s = {1, 2, 3, 4, 5};// 方式1:使用 find() 查找(返回迭代器)auto it = s.find(3);if (it != s.end()) {cout << "找到元素:" << *it << endl; // 找到元素:3} else {cout << "未找到元素" << endl;}// 方式2:检查元素是否存在(C++20 引入 contains())if (s.contains(4)) { // 等价于 s.find(4) != s.end()cout << "元素 4 存在" << endl;}// 方式3:查找第一个 >= 目标值的元素(lower_bound)auto lower = s.lower_bound(3);cout << "第一个 >= 3 的元素:" << *lower << endl; // 3// 方式4:查找第一个 > 目标值的元素(upper_bound)auto upper = s.upper_bound(3);cout << "第一个 > 3 的元素:" << *upper << endl; // 4return 0;
}
2.4 删除元素
std::set 支持通过值、迭代器或范围删除元素:
cpp
运行
#include <set>
#include <iostream>
using namespace std;int main() {set<int> s = {1, 2, 3, 4, 5};// 方式1:通过值删除(返回删除的元素数量,0 或 1)size_t count = s.erase(3);cout << "删除了 " << count << " 个元素" << endl; // 1// 方式2:通过迭代器删除auto it = s.find(4);if (it != s.end()) {s.erase(it); // 删除元素 4}// 方式3:删除范围 [first, last)auto start = s.find(1);auto end = s.find(5);s.erase(start, end); // 删除元素 1、2(不包含 end 指向的 5)// 方式4:清空所有元素s.clear();cout << "清空后元素数量:" << s.size() << endl; // 0return 0;
}
注意:erase(iterator) 会返回下一个有效迭代器(C++11 起),可安全用于循环删除。
2.5 遍历元素
std::set 是有序容器,遍历操作可充分利用其排序特性:
cpp
运行
#include <set>
#include <iostream>
using namespace std;int main() {set<int> s = {3, 1, 4, 1, 5, 9}; // 去重排序后为 {1,3,4,5,9}// 方式1:范围 for 循环(C++11)cout << "正向遍历:";for (int num : s) {cout << num << " ";}// 输出:正向遍历:1 3 4 5 9 // 方式2:迭代器遍历cout << "\n迭代器遍历:";for (set<int>::iterator it = s.begin(); it != s.end(); ++it) {cout << *it << " ";}// 输出:迭代器遍历:1 3 4 5 9 // 方式3:反向迭代器(从大到小)cout << "\n反向遍历:";for (auto it = s.rbegin(); it != s.rend(); ++it) {cout << *it << " ";}// 输出:反向遍历:9 5 4 3 1 return 0;
}
三、set 的比较器与自定义元素类型
3.1 自定义比较器
std::set 默认使用 std::less<T> 按升序排序,可通过自定义比较器改变排序规则:
cpp
运行
#include <set>
#include <iostream>
#include <string>
using namespace std;// 自定义比较器:按字符串长度降序排列
struct StrLenCompare {bool operator()(const string& a, const string& b) const {// 长度不同时,长的在前if (a.size() != b.size()) {return a.size() > b.size();}// 长度相同时,按字典序升序return a < b;}
};int main() {// 使用自定义比较器的 setset<string, StrLenCompare> s;s.insert("apple"); // 5 个字符s.insert("banana"); // 6 个字符s.insert("pear"); // 4 个字符s.insert("grape"); // 5 个字符(与 apple 长度相同)// 遍历输出(按长度降序,长度相同则字典序升序)for (const string& str : s) {cout << str << "(长度:" << str.size() << ")" << endl;}// 输出:// banana(长度:6)// apple(长度:5)// grape(长度:5)// pear(长度:4)return 0;
}
常用比较器:
std::less<T>:默认,升序std::greater<T>:降序(需包含<functional>)- 自定义结构体:支持复杂排序逻辑(如多字段排序)
3.2 存储自定义类型元素
std::set 可存储自定义类型元素,但需定义比较规则(通过比较器或重载 < 运算符):
cpp
运行
#include <set>
#include <iostream>
#include <string>
using namespace std;// 自定义类型:学生
struct Student {int id;string name;int score;// 重载 < 运算符(用于默认排序)bool operator<(const Student& other) const {// 先按分数降序,分数相同按 id 升序if (score != other.score) {return score > other.score; // 分数高的在前}return id < other.id; // 分数相同则 id 小的在前}
};int main() {set<Student> students;students.insert({1001, "Alice", 90});students.insert({1002, "Bob", 85});students.insert({1003, "Charlie", 90}); // 与 Alice 分数相同// 遍历输出(按分数降序,同分数按 id 升序)for (const auto& stu : students) {cout << "ID: " << stu.id << ", 姓名: " << stu.name << ", 分数: " << stu.score << endl;}// 输出:// ID: 1001, 姓名: Alice, 分数: 90// ID: 1003, 姓名: Charlie, 分数: 90// ID: 1002, 姓名: Bob, 分数: 85return 0;
}
关键要求:比较规则必须满足严格弱序(Strict Weak Ordering),即:
- 非自反性:
a < a为 false - 传递性:若
a < b且b < c,则a < c - 对称性:若
!(a < b)且!(b < a),则a与b视为等价(在 set 中会被视为重复元素)
四、set 的高级操作与应用场景
4.1 范围查询
利用 set 的有序性,可高效查询特定范围内的元素:
cpp
运行
#include <set>
#include <iostream>
using namespace std;int main() {set<int> s = {1, 2, 3, 4, 5, 6, 7, 8, 9};// 查询 [3, 7) 范围内的元素(即 3,4,5,6)auto lower = s.lower_bound(3); // 第一个 >= 3 的元素auto upper = s.upper_bound(6); // 第一个 > 6 的元素cout << "范围 [3,7) 内的元素:";for (auto it = lower; it != upper; ++it) {cout << *it << " ";}// 输出:3 4 5 6 return 0;
}
范围查询的时间复杂度:O (log n + k),其中 n 是容器大小,k 是范围内的元素数量。
4.2 与其他容器的转换
set 可与序列式容器(如 vector)相互转换,实现去重和排序:
cpp
运行
#include <set>
#include <vector>
#include <iostream>
using namespace std;int main() {// 1. vector → set(去重排序)vector<int> vec = {3, 1, 4, 1, 5, 9, 2, 6};set<int> s(vec.begin(), vec.end()); // 去重排序为 {1,2,3,4,5,6,9}// 2. set → vector(获取有序无重复的序列)vector<int> sorted_vec(s.begin(), s.end());for (int num : sorted_vec) {cout << num << " ";}// 输出:1 2 3 4 5 6 9 return 0;
}
4.3 常见应用场景
去重与排序快速对序列去重并按特定规则排序,替代
vector + sort + unique的组合:cpp
运行
// 传统方式:vector 去重排序 vector<int> vec = {3,1,4,1,5}; sort(vec.begin(), vec.end()); auto last = unique(vec.begin(), vec.end()); vec.erase(last, vec.end());// 更简洁的方式:使用 set set<int> s(vec.begin(), vec.end()); // 一步完成去重排序集合运算利用
set的有序性实现交集、并集、差集等集合操作:cpp
运行
// 求两个 set 的交集 set<int> s1 = {1,2,3,4}, s2 = {3,4,5,6}; set<int> intersection; set_intersection(s1.begin(), s1.end(), s2.begin(), s2.end(),inserter(intersection, intersection.begin())); // intersection: {3,4}有序数据的动态维护适用于需要频繁插入、删除并保持有序的场景(如排行榜、日程安排):
cpp
运行
// 维护一个动态有序的分数列表 set<int, greater<int>> scores; // 按分数降序 scores.insert(90); scores.insert(85); scores.insert(95); // 前三名是 95,90,85(直接通过迭代器访问)
五、性能分析与最佳实践
5.1 时间复杂度
std::set 核心操作的时间复杂度(n 为元素数量):
- 插入(
insert):O(log n) - 查找(
find、contains):O(log n) - 删除(
erase):O(log n) - 范围查询(
lower_bound到upper_bound):O(log n + k) - 遍历(
begin()到end()):O(n)
5.2 空间复杂度
std::set 的空间复杂度为 O (n),每个元素需额外存储红黑树节点的指针(父节点、左右子节点)和颜色标记,内存开销略高于 vector,但低于 unordered_set(无哈希表开销)。
5.3 最佳实践
优先使用
contains()检查元素是否存在(C++20)比find() != end()更简洁直观:cpp
运行
if (s.contains(5)) { // 等价于 s.find(5) != s.end()// 元素存在 }批量插入时使用范围插入范围插入(
insert(first, last))比逐个插入更高效,红黑树可一次性完成平衡调整:cpp
运行
vector<int> data = {1,2,3,4,5}; s.insert(data.begin(), data.end()); // 高效批量插入避免修改元素的值
set中的元素是const类型(修改会破坏排序),若需修改,需先删除旧元素再插入新元素:cpp
运行
// 错误:元素是 const,无法直接修改 *s.find(2) = 3; // 编译错误// 正确:删除旧元素,插入新元素 auto it = s.find(2); if (it != s.end()) {s.erase(it);s.insert(3); }根据场景选择
set与unordered_set- 需要有序性或范围查询 → 选
set - 仅需去重且追求极致查找效率 → 选
unordered_set
- 需要有序性或范围查询 → 选
六、multiset:允许重复元素的有序集合
std::multiset 是 set 的变体,允许元素重复,其他特性与 set 一致:
cpp
运行
#include <set>
#include <iostream>
using namespace std;int main() {multiset<int> ms = {3, 1, 2, 2, 3, 3}; // 允许重复,自动排序// 遍历输出cout << "所有元素:";for (int num : ms) {cout << num << " ";}// 输出:1 2 2 3 3 3 // 统计元素出现次数int target = 3;auto range = ms.equal_range(target); // 返回 [lower, upper) 迭代器对int count = distance(range.first, range.second); // 计算范围内元素数cout << "\n元素 " << target << " 出现了 " << count << " 次" << endl; // 3 次// 删除所有值为 2 的元素ms.erase(2); // erase(value) 删除所有等于 value 的元素cout << "删除 2 后:";for (int num : ms) {cout << num << " ";}// 输出:1 3 3 3 return 0;
}
注意:multiset::erase(value) 会删除所有等于 value 的元素,若仅需删除一个,需先通过迭代器定位。
七、总结
std::set 是一种基于红黑树的有序容器,核心特性是元素唯一且自动排序,提供 O (log n) 时间复杂度的插入、查找和删除操作。其优势在于:
- 无需手动维护排序状态,插入时自动保持有序
- 高效的范围查询能力,适合需要区间操作的场景
- 天然支持去重,简化数据处理逻辑
在使用 set 时,需注意元素的 const 特性(不可直接修改)和比较器的严格弱序要求。对于允许重复元素的场景,可使用 multiset;对于无需有序性的场景,unordered_set 通常提供更高的查找效率。
掌握 std::set 的使用,能帮助开发者更优雅地处理有序去重、范围查询等问题,提升代码的可读性和效率。
