目录
在 C++ 编程中,容器是非常重要的工具,它可以帮助我们高效地管理和操作数据。关联容器是 C++ 标准模板库(STL)中的一类特殊容器,它们通过键(key)来存储和访问元素。set
作为关联容器的一种,在很多场景下都有着广泛的应用。本文将全面深入地介绍 set
类型,包括其基本概念、底层实现、常用操作、高级应用以及与其他容器的比较等内容。
一、set 基本概念
1.1 定义与特点
set
是一种关联容器,它用于存储唯一的元素,并且这些元素会根据其值自动进行排序。在 set
中,键(key)和值(value)是相同的,即每个元素既是键也是值。由于 set
不允许有重复的元素,因此插入重复元素时会被忽略。
1.2 头文件与声明
要使用 set
,需要包含 <set>
头文件。以下是一个简单的 set
声明示例:
#include <iostream>
#include <set>
int main() {
std::set<int> mySet; // 声明一个存储 int 类型元素的 set
return 0;
}
1.3 核心特性解析
std::set
是基于红黑树实现的有序关联容器,其设计目标是通过平衡二叉搜索树结构,在保证元素唯一性的同时,实现以下核心特性:
①自动排序机制
- 插入元素时自动按升序排列
- 默认使用
<
运算符比较元素 - 支持自定义比较函数
②唯一性约束
- 不允许重复元素
- 插入重复值时自动去重
③对数时间复杂度操作
- 插入:O(log n)
- 删除:O(log n)
- 查找:O(log n)
④内存动态管理
- 自动处理内存分配/释放
- 支持迭代器失效保护
二、set 底层实现
2.1 红黑树简介
set
通常使用红黑树(Red-Black Tree)作为底层数据结构。红黑树是一种自平衡的二叉搜索树,它通过对节点进行着色(红色或黑色)并遵循一些特定的规则来保持树的平衡,从而保证了插入、删除和查找操作的时间复杂度都是 O(logn)。
红黑树的主要特性包括:
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色。
- 每个叶子节点(NIL 节点,空节点)是黑色。
- 如果一个节点是红色的,则它的两个子节点都是黑色的。
- 对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点。
2.2 红黑树在 set 中的应用
在 set
中,红黑树的节点存储了 set
中的元素。当插入一个新元素时,set
会根据元素的值将其插入到红黑树的合适位置,并通过旋转和着色操作来保持树的平衡。查找元素时,set
会从根节点开始,根据元素的值与节点的值进行比较,然后递归地在左子树或右子树中查找,直到找到目标元素或到达叶子节点。
三、set 常用操作
3.1 插入元素
可以使用 insert()
函数向 set
中插入元素。如果插入的元素已经存在于 set
中,则插入操作会被忽略。
#include <iostream>
#include <set>
int main() {
std::set<int> mySet;
mySet.insert(3);
mySet.insert(1);
mySet.insert(2);
for (const auto& element : mySet) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
输出:1 2 3
,因为 set
会自动对元素进行排序。
3.2 删除元素
可以使用 erase()
函数从 set
中删除元素。erase()
函数有多种重载形式,可以根据元素的值或迭代器来删除元素。
#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {1, 2, 3, 4, 5};
mySet.erase(3); // 根据元素的值删除
auto it = mySet.find(4);
if (it != mySet.end()) {
mySet.erase(it); // 根据迭代器删除
}
for (const auto& element : mySet) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
3.3 查找元素
可以使用 find()
函数在 set
中查找元素。如果找到元素,则返回指向该元素的迭代器;如果未找到,则返回 end()
迭代器。
#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {1, 2, 3, 4, 5};
auto it = mySet.find(3);
if (it != mySet.end()) {
std::cout << "Found: " << *it << std::endl;
} else {
std::cout << "Not found" << std::endl;
}
return 0;
}
3.4 遍历元素
可以使用范围 for
循环或迭代器来遍历 set
中的元素。
#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {1, 2, 3, 4, 5};
// 使用范围 for 循环遍历
for (const auto& element : mySet) {
std::cout << element << " ";
}
std::cout << std::endl;
// 使用迭代器遍历
for (auto it = mySet.begin(); it != mySet.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
3.5 性能特征
操作 | 时间复杂度 | 说明 |
---|---|---|
insert() | O(log n) | 插入新元素 |
erase() | O(log n) | 删除元素 |
find() | O(log n) | 查找元素 |
count() | O(log n) | 统计元素出现次数 |
size() | O(1) | 获取元素数量 |
empty() | O(1) | 检查是否为空 |
四、set 高级应用
4.1 自定义比较函数
默认情况下,set
使用 std::less
作为比较函数来对元素进行排序。如果需要自定义排序规则,可以在声明 set
时提供自定义的比较函数。
#include <iostream>
#include <set>
// 自定义比较函数,实现降序排序
struct Greater {
bool operator()(const int& a, const int& b) const {
return a > b;
}
};
int main() {
std::set<int, Greater> mySet = {1, 2, 3, 4, 5};
for (const auto& element : mySet) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
4.2 交集、并集和差集操作
可以使用 <algorithm>
头文件中的 set_intersection()
、set_union()
和 set_difference()
函数来实现 set
的交集、并集和差集操作。
#include <iostream>
#include <set>
#include <algorithm>
#include <vector>
int main() {
std::set<int> set1 = {1, 2, 3, 4, 5};
std::set<int> set2 = {3, 4, 5, 6, 7};
std::vector<int> intersection;
std::set_intersection(set1.begin(), set1.end(), set2.begin(), set2.end(), std::back_inserter(intersection));
std::cout << "Intersection: ";
for (const auto& element : intersection) {
std::cout << element << " ";
}
std::cout << std::endl;
std::vector<int> unionSet;
std::set_union(set1.begin(), set1.end(), set2.begin(), set2.end(), std::back_inserter(unionSet));
std::cout << "Union: ";
for (const auto& element : unionSet) {
std::cout << element << " ";
}
std::cout << std::endl;
std::vector<int> difference;
std::set_difference(set1.begin(), set1.end(), set2.begin(), set2.end(), std::back_inserter(difference));
std::cout << "Difference (set1 - set2): ";
for (const auto& element : difference) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
4.3 与其他容器的结合使用
set
可以与其他容器(如 vector
、list
等)结合使用,以实现更复杂的功能。例如,可以将 vector
中的元素插入到 set
中进行去重和排序。
#include <iostream>
#include <set>
#include <vector>
int main() {
std::vector<int> vec = {3, 1, 2, 3, 4, 2};
std::set<int> mySet(vec.begin(), vec.end());
for (const auto& element : mySet) {
std::cout << element << " ";
}
std::cout << std::endl;
return 0;
}
4.4 迭代器操作
std::set<int> s = {1, 3, 5, 7, 9};
// 反向迭代器
std::cout << "反向遍历: ";
for (auto rit = s.rbegin(); rit != s.rend(); ++rit) {
std::cout << *rit << " ";
}
// 迭代器失效处理
auto it = s.find(5);
s.erase(it); // 迭代器失效,不可继续使用
4.5 性能优化技巧
批量插入优化:
std::set<int> s;
s.insert({1, 2, 3, 4, 5}); // C++11初始化列表优化
emplace原位构造:
s.emplace(10); // 直接在容器内构造对象,避免临时对象拷贝
预留空间(C++11起):
s.reserve(100); // 预分配内存减少重分配次数
五、set 与其他容器的比较
操作 | set | unordered_set | vector有序 |
---|---|---|---|
插入 | O(log n) | O(1)平均 | O(n) |
查找 | O(log n) | O(1)平均 | O(log n)二分 |
删除 | O(log n) | O(1)平均 | O(n) |
内存占用 | 较高 | 较低 | 紧凑 |
有序性 | 始终有序 | 无序 | 需排序 |
5.1 set 与 vector
- 存储方式:
vector
是一种顺序容器,它使用连续的内存空间存储元素;而set
是一种关联容器,使用红黑树存储元素。 - 元素特性:
vector
允许有重复的元素,并且元素的顺序是按照插入的顺序排列的;set
不允许有重复的元素,并且元素会自动排序。 - 查找效率:
vector
的查找操作的时间复杂度为 O(n),而set
的查找操作的时间复杂度为 O(logn)。
5.2 set 与 unordered_set
- 底层实现:
set
使用红黑树作为底层数据结构,而unordered_set
使用哈希表作为底层数据结构。 - 排序特性:
set
中的元素会自动排序,而unordered_set
中的元素是无序的。 - 查找效率:
unordered_set
的查找操作的平均时间复杂度为 O(1),而set
的查找操作的时间复杂度为 O(logn)。
5.3 选择建议
- 如果需要存储唯一的元素并且要求元素有序,那么可以选择
set
。 - 如果只需要存储唯一的元素,不关心元素的顺序,并且对查找效率有较高的要求,那么可以选择
unordered_set
。 - 如果需要存储多个相同的元素,并且对元素的顺序有要求,那么可以选择
vector
。
六、注意事项与常见错误
6.1 迭代器失效问题
在对 set
进行插入或删除操作时,可能会导致迭代器失效。例如,在删除一个元素后,指向该元素的迭代器将失效。因此,在使用迭代器时,需要特别注意避免迭代器失效的问题。
#include <iostream>
#include <set>
int main() {
std::set<int> mySet = {1, 2, 3, 4, 5};
auto it = mySet.find(3);
if (it != mySet.end()) {
mySet.erase(it); // 删除元素后,it 迭代器失效
// 不能再使用 it 迭代器
}
return 0;
}
6.2 性能考虑
虽然 set
的插入、删除和查找操作的时间复杂度都是 O(logn),但在某些情况下,可能会有性能瓶颈。例如,当需要频繁插入和删除元素时,红黑树的旋转和着色操作可能会影响性能。此时,可以考虑使用 unordered_set
来提高性能。
6.3 内存占用
由于 set
使用红黑树作为底层数据结构,它需要额外的内存来存储节点的指针和颜色信息。因此,在对内存占用有严格要求的场景下,需要谨慎使用 set
。
6.4 易错点提醒
修改元素值:直接修改元素会导致未定义行为
auto it = s.begin();
// *it = 10; // 错误!元素是const的
迭代器失效:仅删除操作会使指向被删元素的迭代器失效
自定义比较函数:需保证严格弱序关系
// 错误示例:未实现严格弱序
struct BadCompare {
bool operator()(int a, int b) {
return a <= b; // 应该用<
}
};
七、应用场景与实战案例
案例1:数据去重排序
std::vector<int> nums = {5, 2, 5, 1, 3, 2};
std::set<int> unique_nums(nums.begin(), nums.end());
// 输出:1 2 3 5
for(int num : unique_nums) {
std::cout << num << " ";
}
案例2:字典序管理
std::set<std::string> dictionary;
void add_word(const std::string& word) {
dictionary.insert(word);
}
bool check_spelling(const std::string& word) {
return dictionary.count(word);
}
八、总结
set
作为STL中的有序唯一集合容器,在需要维护有序数据且保证元素唯一性的场景中表现卓越。通过红黑树实现的高效操作使其成为处理排序、去重、快速查找等需求的理想选择。掌握set
的特性和正确使用方式,将显著提升C++编程能力。
九、参考资料
- 《C++ Primer(第 5 版)》这本书是 C++ 领域的经典之作,对 C++ 的基础语法和高级特性都有深入讲解。
- 《Effective C++(第 3 版)》书中包含了很多 C++ 编程的实用建议和最佳实践。
- 《C++ Templates: The Complete Guide(第 2 版)》该书聚焦于 C++ 模板编程,而
using
声明在模板编程中有着重要应用,如定义模板类型别名等。 - C++ 官方标准文档:C++ 标准文档是最权威的参考资料,可以查阅最新的 C++ 标准(如 C++11、C++14、C++17、C++20 等)文档。例如,ISO/IEC 14882:2020 是 C++20 标准的文档,可从相关渠道获取其详细内容。