C++ set 容器全方位解析:从基础用法到实战示例
在 C++ STL(标准模板库)中,set 是一种非常实用的关联式容器,它基于红黑树(平衡二叉搜索树)实现,具备自动排序和去重的核心特性。本文将结合具体代码示例,从基础用法、核心特性到实战场景,带你全面掌握 set 的使用。
一、set 容器的核心特性
在学习用法之前,先明确 set 的几个关键特性,这是理解其行为的基础:
- 自动排序:
set会根据元素的默认比较规则(less<T>,即升序)对插入的元素自动排序,无需手动调用sort。 - 元素唯一:
set不允许存储重复元素,若插入已存在的元素,会自动忽略(插入操作返回的pair中,second为false)。 - 基于红黑树:底层实现保证了插入、删除、查找操作的时间复杂度均为 O(log n),效率极高。
- 迭代器特性:
set的迭代器是双向迭代器(bidirectional iterator),支持++、--操作,但不支持随机访问(如it + 1)。
二、set 基础用法实战:从代码看本质
我们先从你提供的代码入手,逐步拆解 set 的核心操作,理解每个步骤背后的逻辑。
1. 代码重现与运行结果
先看完整代码的运行效果,再逐一分析:
cpp
#include <iostream>
#include <set>
using namespace std;
void set_test()
{
// 1. 定义一个存储 int 类型的 set
set<int> s;
// 2. 插入元素(自动去重+排序)
s.insert(1);
s.insert(8);
s.insert(7);
s.insert(3);
s.insert(2);
s.insert(1); // 插入重复元素,会被自动忽略
// 3. 遍历 set(输出排序后的结果)
cout << "插入元素后,set 内容:";
for (auto& e : s)
{
cout << e << " "; // 输出:1 2 3 7 8
}
cout << endl;
// 4. 查找指定元素的下界(lower_bound)
set<int>::iterator sit = s.lower_bound(3); // 找到第一个 >= 3 的元素(即 3)
// 5. 区间删除(删除 [begin(), sit) 范围内的元素)
auto it = s.begin(); // 指向第一个元素(1)
s.erase(it, sit); // 删除 1、2,保留 3、7、8
// 6. 再次遍历,查看删除后的结果
cout << "删除元素后,set 内容:";
for (auto& e : s)
{
cout << e << " "; // 输出:3 7 8
}
}
int main()
{
set_test();
return 0;
}
运行结果:
plaintext
插入元素后,set 内容:1 2 3 7 8
删除元素后,set 内容:3 7 8
2. 关键操作解析
(1)定义与初始化
set 的定义格式为 set<元素类型> 容器名,支持多种初始化方式:
cpp
// 方式 1:默认初始化(空 set)
set<int> s1;
// 方式 2:用初始化列表初始化(C++11 及以上)
set<int> s2 = {5, 2, 9, 2}; // 自动去重+排序,结果:2 5 9
// 方式 3:用其他容器的迭代器初始化
vector<int> vec = {3, 1, 4};
set<int> s3(vec.begin(), vec.end()); // 结果:1 3 4
(2)插入操作(insert)
insert 是 set 最常用的操作之一,核心作用是 “插入元素并维持排序和去重”。它的返回值是一个 pair<iterator, bool>:
iterator:指向插入后的元素(若元素已存在,则指向已有的那个元素)。bool:true表示插入成功,false表示元素已存在,插入失败。
示例:
cpp
set<int> s;
// 插入单个元素
auto ret1 = s.insert(5);
cout << "插入 5:" << (ret1.second ? "成功" : "失败") << endl; // 成功
// 插入重复元素
auto ret2 = s.insert(5);
cout << "插入 5:" << (ret2.second ? "成功" : "失败") << endl; // 失败
// 插入多个元素(C++11 支持的初始化列表)
s.insert({3, 7, 3}); // 自动去重,结果:3 5 7
(3)遍历操作
set 支持两种遍历方式:范围 for 循环(简洁)和 迭代器遍历(灵活)。
-
范围 for 循环(推荐,C++11 及以上):
cpp
for (auto& e : s) // 用引用避免拷贝,提高效率 { cout << e << " "; } -
迭代器遍历(适合需要灵活控制位置的场景):
cpp
// 正向遍历 set<int>::iterator it = s.begin(); while (it != s.end()) { cout << *it << " "; ++it; // 双向迭代器,仅支持 ++,不支持 it += 1 } // 反向遍历(用 reverse_iterator) set<int>::reverse_iterator rit = s.rbegin(); while (rit != s.rend()) { cout << *rit << " "; // 从大到小输出 ++rit; }
(4)查找操作(find /lower_bound/upper_bound)
set 提供了多个查找相关的成员函数,比通用算法 std::find 效率更高(O (log n) vs O (n)):
| 函数名 | 功能描述 | 返回值 |
|---|---|---|
find(val) |
查找值为 val 的元素 |
找到则返回迭代器,否则返回 end() |
lower_bound(val) |
查找第一个 >= val 的元素 |
迭代器 |
upper_bound(val) |
查找第一个 > val 的元素 |
迭代器 |
示例(基于你代码中的 lower_bound 场景):
cpp
set<int> s = {1, 2, 3, 7, 8};
// 1. find:查找 3
auto it1 = s.find(3);
if (it1 != s.end())
{
cout << "找到元素:" << *it1 << endl; // 输出:3
}
// 2. lower_bound:查找第一个 >= 3 的元素(即 3)
auto it2 = s.lower_bound(3);
cout << "lower_bound(3):" << *it2 << endl; // 输出:3
// 3. upper_bound:查找第一个 > 3 的元素(即 7)
auto it3 = s.upper_bound(3);
cout << "upper_bound(3):" << *it3 << endl; // 输出:7
(5)删除操作(erase)
erase 支持三种删除方式,满足不同场景的需求:
-
删除指定元素(按值):
cpp
set<int> s = {1, 2, 3, 7, 8}; s.erase(2); // 删除值为 2 的元素,结果:1 3 7 8 -
删除指定位置的元素(按迭代器):
cpp
auto it = s.find(3); if (it != s.end()) { s.erase(it); // 删除迭代器指向的元素(3),结果:1 7 8 } -
删除区间内的元素(按迭代器范围):这就是你代码中用到的方式,格式为
erase(begin_it, end_it),删除 [begin_it, end_it) 范围内的元素(左闭右开):cpp
set<int> s = {1, 2, 3, 7, 8}; auto start = s.begin(); // 指向 1 auto end = s.lower_bound(3); // 指向 3 s.erase(start, end); // 删除 1、2,结果:3 7 8
(6)其他常用操作
除了上述核心操作,set 还有几个常用的成员函数:
cpp
set<int> s = {1, 2, 3, 7, 8};
cout << "元素个数:" << s.size() << endl; // 输出:5
cout << "是否为空:" << (s.empty() ? "是" : "否") << endl; // 输出:否
s.clear(); // 清空所有元素
cout << "清空后元素个数:" << s.size() << endl; // 输出:0
三、set 的进阶特性:自定义排序规则
默认情况下,set 按 less<T> 升序排序,但实际开发中可能需要自定义排序(如降序、按字符串长度排序等)。此时可以通过 “指定比较函数” 来实现。
1. 基础类型的自定义排序(如降序)
对于 int、string 等基础类型,可直接使用 STL 提供的 greater<T> 作为比较规则:
cpp
#include <functional> // 需包含此头文件,以使用 greater
set<int, greater<int>> s; // 按降序排序
s.insert({1, 8, 7, 3, 2});
cout << "降序 set:";
for (auto& e : s)
{
cout << e << " "; // 输出:8 7 3 2 1
}
2. 自定义类型的排序(如结构体)
如果 set 存储的是自定义结构体,需要显式定义比较规则(通过函数对象或 lambda 表达式)。
示例:存储学生结构体,按成绩降序排序,成绩相同则按姓名升序排序:
cpp
#include <string>
#include <set>
// 自定义结构体
struct Student
{
string name;
int score;
};
// 自定义比较函数对象(按成绩降序,成绩相同按姓名升序)
struct CompareStudent
{
bool operator()(const Student& a, const Student& b) const
{
if (a.score != b.score)
{
return a.score > b.score; // 成绩降序
}
else
{
return a.name < b.name; // 姓名升序
}
}
};
int main()
{
set<Student, CompareStudent> s;
s.insert({"Alice", 90});
s.insert({"Bob", 85});
s.insert({"Charlie", 90}); // 成绩与 Alice 相同,按姓名排序
// 遍历输出
for (auto& stu : s)
{
cout << "姓名:" << stu.name << ",成绩:" << stu.score << endl;
}
return 0;
}
运行结果:
plaintext
姓名:Alice,成绩:90
姓名:Charlie,成绩:90
姓名:Bob,成绩:85
四、set 的注意事项
-
元素不可修改:
set中的元素是 “常量”,不能通过迭代器修改元素的值(因为修改会破坏红黑树的排序结构)。若需修改元素,需先删除旧元素,再插入新元素。cpp
set<int> s = {1, 2, 3}; auto it = s.find(2); // *it = 5; // 错误!set 的元素不可修改 -
迭代器失效问题:
set的插入、删除操作不会导致其他迭代器失效(仅被删除的迭代器失效),这是红黑树结构的特性,使用时无需过度担心迭代器失效问题
五、总结
set 是 C++ 中非常实用的容器,核心优势在于自动排序和高效的插入 / 查找 / 删除,适合需要 “有序 + 唯一” 元素的场景(如统计去重后的有序数据、区间查询等)。
更多推荐


所有评论(0)