C++中的set容器:有序、唯一元素的高效管理
在C++标准模板库(STL)中,set是一种基于红黑树(Red-Black Tree) 实现的关联容器,其核心特性是元素有序且唯一。它支持高效的插入、删除和查找操作,适用于需要快速检索且元素不重复的场景。本文将详细解析set的特性、用法、底层实现及实际应用,帮助开发者掌握这一常用容器。
一、set的核心特性与定义
set容器的本质是有序且无重复元素的集合,其核心特性如下:
- 元素唯一性:
set中不会存储重复元素,插入已存在的元素会被自动忽略。 - 自动排序:元素在
set中按特定规则(默认升序)自动排序,无需手动维护顺序。 - 基于红黑树:底层采用平衡二叉搜索树(红黑树)实现,插入、删除、查找的平均时间复杂度为O(log n)。
- 不可直接修改元素:
set的元素被视为const(常量),无法直接修改(若需修改,需先删除旧元素再插入新元素)。
set的定义与头文件
使用set需包含头文件<set>,并通过命名空间std访问。其模板定义如下:
template <
class T, // 元素类型
class Compare = std::less<T>, // 比较器(默认升序)
class Allocator = std::allocator<T> // 分配器(默认即可)
> class set;
基本定义示例:
#include <set>
using namespace std;
// 存储int类型,默认按less<int>(升序)排序
set<int> s1;
// 存储string类型,按降序排序(使用greater<string>)
set<string, greater<string>> s2;
二、set的基本操作
set提供了丰富的成员函数,涵盖元素的插入、删除、查找、遍历等常用操作。
1. 插入元素(insert)
insert函数用于向set中添加元素,若元素已存在则插入失败(返回值可判断是否插入成功)。
用法:
set<int> s;
// 插入单个元素
s.insert(3);
s.insert(1);
s.insert(2);
s.insert(2); // 重复元素,插入失败
// 插入多个元素(C++11初始化列表)
s.insert({5, 4, 6});
// 插入返回值:pair<iterator, bool>
// - first:指向插入元素或已存在元素的迭代器
// - second:插入成功为true,失败为false
auto result = s.insert(7);
if (result.second) {
cout << "插入成功:" << *result.first << endl;
} else {
cout << "插入失败(元素已存在):" << *result.first << endl;
}
插入后集合状态:{1, 2, 3, 4, 5, 6, 7}(自动按升序排序)。
2. 删除元素(erase)
erase函数用于删除set中的元素,支持按值、迭代器或范围删除。
用法:
set<int> s = {1, 2, 3, 4, 5};
// (1)按值删除:返回删除的元素个数(0或1,因元素唯一)
size_t count = s.erase(3); // 删除值为3的元素,返回1
cout << "删除了" << count << "个元素" << endl;
// (2)按迭代器删除:删除迭代器指向的元素
auto it = s.find(4); // 查找值为4的元素
if (it != s.end()) {
s.erase(it); // 删除迭代器指向的元素
}
// (3)删除范围:删除[first, last)之间的元素
it = s.find(2);
s.erase(it, s.end()); // 删除从2开始到末尾的元素(即2,5)
// 此时集合状态:{1}
3. 查找元素(find)
find函数用于查找指定值的元素,返回指向该元素的迭代器;若未找到,返回end()。
用法:
set<string> fruits = {"apple", "banana", "orange"};
// 查找元素
auto it = fruits.find("banana");
if (it != fruits.end()) {
cout << "找到元素:" << *it << endl; // 输出:找到元素:banana
} else {
cout << "未找到元素" << endl;
}
4. 其他常用操作
| 函数 | 功能描述 |
|---|---|
size() | 返回当前元素个数 |
empty() | 判断容器是否为空(空返回true) |
clear() | 清空所有元素 |
count(val) | 返回值为val的元素个数(0或1,因元素唯一) |
lower_bound(val) | 返回第一个不小于val的元素迭代器 |
upper_bound(val) | 返回第一个大于val的元素迭代器 |
begin()/end() | 返回首元素/尾后迭代器(用于遍历) |
示例:
set<int> s = {2, 1, 4, 3};
cout << "元素个数:" << s.size() << endl; // 输出:4
// 遍历元素(默认升序)
for (auto it = s.begin(); it != s.end(); ++it) {
cout << *it << " "; // 输出:1 2 3 4
}
cout << endl;
// lower_bound与upper_bound
auto low = s.lower_bound(2); // 指向2
auto high = s.upper_bound(3); // 指向4
cout << "lower_bound(2):" << *low << endl; // 2
cout << "upper_bound(3):" << *high << endl; // 4
三、set的排序规则:自定义比较器
set默认使用std::less<T>作为比较器,对元素按升序排序。若需自定义排序规则(如降序、按元素某字段排序),可通过以下两种方式实现:
1. 使用预定义比较器(如greater)
std::greater<T>是STL提供的另一种比较器,可实现降序排序。
示例:
#include <set>
#include <functional> // 包含greater
set<int, greater<int>> s = {3, 1, 4, 2};
// 遍历(降序)
for (int num : s) { // 范围for循环(C++11)
cout << num << " "; // 输出:4 3 2 1
}
2. 自定义比较器(函数对象或lambda)
对于复杂类型(如自定义结构体),需定义比较规则。可通过函数对象(Functor) 或lambda表达式实现。
(1)函数对象(适用于C++11前)
#include <string>
// 自定义结构体
struct Person {
string name;
int age;
Person(string n, int a) : name(n), age(a) {}
};
// 自定义比较器:按年龄升序(年龄相同则按姓名升序)
struct ComparePerson {
bool operator()(const Person& p1, const Person& p2) const {
if (p1.age != p2.age) {
return p1.age < p2.age; // 年龄小的在前
} else {
return p1.name < p2.name; // 年龄相同则姓名字典序
}
}
};
// 使用自定义比较器的set
set<Person, ComparePerson> people;
int main() {
people.insert(Person("Alice", 25));
people.insert(Person("Bob", 20));
people.insert(Person("Charlie", 25));
// 遍历(按年龄升序,同年龄按姓名升序)
for (const auto& p : people) {
cout << p.name << "(" << p.age << "), ";
}
// 输出:Bob(20), Alice(25), Charlie(25),
return 0;
}
(2)lambda表达式(C++11及以上,配合decltype)
对于简单场景,可直接用lambda表达式定义比较规则,但需注意:lambda作为模板参数时需用decltype推导类型,并显式传递给构造函数。
#include <set>
int main() {
// 定义lambda比较器:按字符串长度降序
auto cmp = [](const string& a, const string& b) {
return a.size() > b.size();
};
// 声明set:用decltype获取lambda类型
set<string, decltype(cmp)> words(cmp);
words.insert({"apple", "banana", "pear", "grape"});
// 遍历(按长度降序:banana(6), apple(5), grape(5), pear(4))
for (const auto& w : words) {
cout << w << "(" << w.size() << "), ";
}
return 0;
}
四、set与multiset的区别
multiset是set的变体,唯一区别是允许存储重复元素,其他特性(如有序、基于红黑树)完全一致。
multiset的特殊点:
insert函数总是成功(允许重复);count(val)返回值为val的元素个数(可能大于1);erase(val)会删除所有值为val的元素(若需删除单个,需通过迭代器)。
示例:
#include <set>
int main() {
multiset<int> ms = {2, 1, 2, 3, 2};
// 遍历(有序,保留重复):1 2 2 2 3
for (int num : ms) {
cout << num << " ";
}
cout << endl;
cout << "值为2的元素个数:" << ms.count(2) << endl; // 输出:3
// 删除所有值为2的元素
ms.erase(2);
// 此时集合:1 3
return 0;
}
五、set的底层实现:红黑树
set的高效性能源于其底层的红黑树实现,这是一种自平衡的二叉搜索树(BST),具有以下特性:
- 二叉搜索树特性:对于任意节点,左子树所有元素小于该节点,右子树所有元素大于该节点(基于比较器),确保查找、插入、删除可通过二分法快速定位(O(log n))。
- 自平衡机制:通过颜色规则(节点分为红/黑)和旋转操作,确保树的高度始终保持在O(log n),避免普通BST在极端情况下退化为链表(O(n)性能)。
- 迭代器特性:
set的迭代器是双向迭代器(可++/–),遍历顺序为中序遍历(左→根→右),因此能按排序规则访问元素。
六、set的适用场景
set适合以下场景:
- 需要自动去重并排序的场景:如存储唯一ID、关键词去重并按字典序排列。
- 高效查找、插入、删除:如日志系统中按时间戳存储事件(需有序且快速检索)。
- 范围查询:利用
lower_bound和upper_bound快速获取区间元素(如查找分数在[60, 80]之间的学生)。
不适用场景:
- 需要频繁修改元素值(需先删除再插入,效率低);
- 追求极致的插入/查找性能且元素无序(可考虑
unordered_set,基于哈希表,平均O(1))。
七、注意事项与最佳实践
-
元素不可直接修改:
set的元素被视为const,若需修改,需先删除旧元素再插入新元素:set<int> s = {1, 2, 3}; // 错误:不能修改迭代器指向的元素 // auto it = s.find(2); *it = 5; // 正确方式:删除旧元素,插入新元素 auto it = s.find(2); if (it != s.end()) { s.erase(it); s.insert(5); // 集合变为{1, 3, 5} } -
比较器的一致性:自定义比较器必须满足严格弱序(Strict Weak Ordering),否则可能导致未定义行为(如插入无限循环、查找错误)。严格弱序要求:
- 反自反性:
cmp(a, a)必须为false; - 传递性:若
cmp(a, b)和cmp(b, c)为true,则cmp(a, c)为true; - 对称性:若
!cmp(a, b)且!cmp(b, a),则a和b视为等价(set中会去重)。
- 反自反性:
-
选择set还是unordered_set:
set:有序、基于红黑树、O(log n)操作、支持范围查询;unordered_set:无序、基于哈希表、平均O(1)操作、不支持范围查询。
根据是否需要有序和范围查询选择,无序且追求速度优先选unordered_set。
八、总结
set是C++ STL中用于管理有序、唯一元素的关联容器,基于红黑树实现,提供O(log n)的插入、删除和查找效率。其核心特性包括自动排序、去重、支持范围查询,适用于需要高效检索且元素无重复的场景。
通过自定义比较器,set可灵活适应不同的排序需求;而multiset作为变体,允许重复元素,扩展了使用场景。在实际开发中,需根据是否需要有序、是否允许重复、性能需求等因素,选择set、multiset或unordered_set。
掌握set的操作和底层原理,能帮助开发者在数据管理场景中写出高效、清晰的代码,是STL容器使用的基础技能之一。
787

被折叠的 条评论
为什么被折叠?



