
在 C++ 编程中,容器是非常重要的工具,它可以帮助我们高效地管理和操作数据。关联容器是 C++ 标准模板库(STL)中的一类特殊容器,它们通过键(key)来存储和访问元素。set 作为关联容器的一种,在很多场景下都有着广泛的应用。本文将全面深入地介绍 set 类型,包括其基本概念、底层实现、常用操作、高级应用以及与其他容器的比较等内容。
set 是一种关联容器,它用于存储唯一的元素,并且这些元素会根据其值自动进行排序。在 set 中,键(key)和值(value)是相同的,即每个元素既是键也是值。由于 set 不允许有重复的元素,因此插入重复元素时会被忽略。
要使用 set,需要包含 <set> 头文件。以下是一个简单的 set 声明示例:
#include <iostream>
#include <set>
int main() {
std::set<int> mySet; // 声明一个存储 int 类型元素的 set
return 0;
}std::set是基于红黑树实现的有序关联容器,其设计目标是通过平衡二叉搜索树结构,在保证元素唯一性的同时,实现以下核心特性:
①自动排序机制
<运算符比较元素②唯一性约束
③对数时间复杂度操作
④内存动态管理
set 通常使用红黑树(Red-Black Tree)作为底层数据结构。红黑树是一种自平衡的二叉搜索树,它通过对节点进行着色(红色或黑色)并遵循一些特定的规则来保持树的平衡,从而保证了插入、删除和查找操作的时间复杂度都是 O(logn)。
红黑树的主要特性包括:
在 set 中,红黑树的节点存储了 set 中的元素。当插入一个新元素时,set 会根据元素的值将其插入到红黑树的合适位置,并通过旋转和着色操作来保持树的平衡。查找元素时,set 会从根节点开始,根据元素的值与节点的值进行比较,然后递归地在左子树或右子树中查找,直到找到目标元素或到达叶子节点。
可以使用 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 会自动对元素进行排序。
可以使用 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;
}可以使用 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;
}可以使用范围 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;
}操作 | 时间复杂度 | 说明 |
|---|---|---|
insert() | O(log n) | 插入新元素 |
erase() | O(log n) | 删除元素 |
find() | O(log n) | 查找元素 |
count() | O(log n) | 统计元素出现次数 |
size() | O(1) | 获取元素数量 |
empty() | O(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;
}可以使用 <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;
}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;
}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); // 迭代器失效,不可继续使用批量插入优化:
std::set<int> s;
s.insert({1, 2, 3, 4, 5}); // C++11初始化列表优化emplace原位构造:
s.emplace(10); // 直接在容器内构造对象,避免临时对象拷贝预留空间(C++11起):
s.reserve(100); // 预分配内存减少重分配次数操作 | 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) |
内存占用 | 较高 | 较低 | 紧凑 |
有序性 | 始终有序 | 无序 | 需排序 |
vector 是一种顺序容器,它使用连续的内存空间存储元素;而 set 是一种关联容器,使用红黑树存储元素。vector 允许有重复的元素,并且元素的顺序是按照插入的顺序排列的;set 不允许有重复的元素,并且元素会自动排序。vector 的查找操作的时间复杂度为 O(n),而 set 的查找操作的时间复杂度为 O(logn)。set 使用红黑树作为底层数据结构,而 unordered_set 使用哈希表作为底层数据结构。set 中的元素会自动排序,而 unordered_set 中的元素是无序的。unordered_set 的查找操作的平均时间复杂度为 O(1),而 set 的查找操作的时间复杂度为 O(logn)。set。unordered_set。vector。在对 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;
}虽然 set 的插入、删除和查找操作的时间复杂度都是 O(logn),但在某些情况下,可能会有性能瓶颈。例如,当需要频繁插入和删除元素时,红黑树的旋转和着色操作可能会影响性能。此时,可以考虑使用 unordered_set 来提高性能。
由于 set 使用红黑树作为底层数据结构,它需要额外的内存来存储节点的指针和颜色信息。因此,在对内存占用有严格要求的场景下,需要谨慎使用 set。
修改元素值:直接修改元素会导致未定义行为
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++编程能力。

using声明在模板编程中有着重要应用,如定义模板类型别名等。