1. 理解std::set的核心特性
在C++标准库中,std::set是一个基于红黑树实现的关联容器,它保持着元素的唯一性和有序性。与vector或list不同,set中的元素会自动按照键值进行排序,这种特性使得它在需要快速查找和去重的场景中表现出色。
set的内部实现通常采用平衡二叉搜索树(最常见的是红黑树),这保证了元素插入、删除和查找操作的时间复杂度都是O(log n)。当我们需要维护一个有序且不重复的元素集合时,set往往是首选容器。
注意:虽然set和unordered_set都是集合容器,但它们的底层实现和特性有本质区别。unordered_set基于哈希表实现,提供平均O(1)的查找性能,但不保持元素有序。
2. set的基本操作与初始化
2.1 创建和初始化set
在C++中使用set需要包含
cpp复制#include <set>
#include <iostream>
int main() {
// 空set
std::set<int> s1;
// 使用初始化列表
std::set<int> s2 = {1, 3, 5, 7, 9};
// 使用迭代器范围初始化
int arr[] = {2, 4, 6, 8};
std::set<int> s3(arr, arr + sizeof(arr)/sizeof(arr[0]));
// 拷贝构造
std::set<int> s4(s2);
// 移动构造(C++11起)
std::set<int> s5(std::move(s2));
return 0;
}
2.2 元素插入操作
set提供了几种插入元素的方法,每种方法在不同场景下有其优势:
cpp复制std::set<std::string> fruitSet;
// 1. insert(value) - 返回pair<iterator, bool>
auto result = fruitSet.insert("apple");
if(result.second) {
std::cout << "Insertion successful\n";
}
// 2. insert(hint, value) - 提供插入位置提示
auto it = fruitSet.begin();
fruitSet.insert(it, "banana"); // 提示可能无效
// 3. insert(first, last) - 范围插入
std::vector<std::string> moreFruits = {"orange", "grape", "pear"};
fruitSet.insert(moreFruits.begin(), moreFruits.end());
// C++11起支持emplace
fruitSet.emplace("mango"); // 避免临时对象构造
提示:insert方法返回的pair中,second成员表示插入是否成功(false表示元素已存在),first成员指向已存在或新插入的元素。
3. set的查找与访问操作
3.1 查找元素
set提供了多种查找方法,适用于不同场景:
cpp复制std::set<int> numSet = {10, 20, 30, 40, 50};
// 1. find - O(log n)查找
auto it = numSet.find(30);
if(it != numSet.end()) {
std::cout << "Found: " << *it << std::endl;
}
// 2. count - 检查存在性(0或1)
if(numSet.count(25) > 0) {
std::cout << "25 exists in set\n";
}
// 3. lower_bound/upper_bound - 范围查询
auto low = numSet.lower_bound(20); // 第一个>=20的元素
auto up = numSet.upper_bound(40); // 第一个>40的元素
// 4. equal_range - 返回匹配范围的pair
auto range = numSet.equal_range(30);
for(auto it = range.first; it != range.second; ++it) {
std::cout << *it << " ";
}
3.2 访问元素
由于set是有序容器,我们可以方便地访问边界元素:
cpp复制std::set<double> values = {3.14, 2.71, 1.618, 0.577};
if(!values.empty()) {
// 访问最小元素
std::cout << "Min: " << *values.begin() << std::endl;
// 访问最大元素
std::cout << "Max: " << *values.rbegin() << std::endl;
// C++11起可以直接获取边界元素
std::cout << "First: " << *values.cbegin() << std::endl;
std::cout << "Last: " << *values.crbegin() << std::endl;
}
注意:set没有提供类似vector的[]运算符,因为set的元素位置由值决定而非索引。
4. set的删除与修改操作
4.1 删除元素
set提供了几种删除元素的方式:
cpp复制std::set<char> charSet = {'a', 'b', 'c', 'd', 'e', 'f'};
// 1. erase by key - 返回删除的元素数量(0或1)
size_t count = charSet.erase('c');
// 2. erase by iterator - 更高效
auto it = charSet.find('d');
if(it != charSet.end()) {
charSet.erase(it);
}
// 3. erase by range
auto first = charSet.lower_bound('b');
auto last = charSet.upper_bound('e');
charSet.erase(first, last);
// 4. clear all elements
charSet.clear();
4.2 修改元素值
由于set元素的键值决定了其在容器中的位置,直接修改元素可能导致容器不一致。正确的方法是:
cpp复制std::set<std::string> nameSet = {"Alice", "Bob", "Charlie"};
// 错误做法:直接修改可能导致问题
// *nameSet.find("Bob") = "Bobby"; // 编译错误或未定义行为
// 正确做法:删除旧值,插入新值
auto it = nameSet.find("Bob");
if(it != nameSet.end()) {
std::string newName = "Bobby";
nameSet.erase(it);
nameSet.insert(newName);
}
5. set的迭代与容量操作
5.1 迭代set元素
set支持多种迭代方式:
cpp复制std::set<int> scores = {95, 88, 92, 78, 85};
// 1. 常规迭代
for(auto it = scores.begin(); it != scores.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
// 2. 范围for循环(C++11)
for(const auto& score : scores) {
std::cout << score << " ";
}
std::cout << std::endl;
// 3. 反向迭代
for(auto rit = scores.rbegin(); rit != scores.rend(); ++rit) {
std::cout << *rit << " ";
}
std::cout << std::endl;
5.2 容量相关操作
set提供了一些查询容器状态的成员函数:
cpp复制std::set<std::string> wordSet;
// 检查是否为空
if(wordSet.empty()) {
std::cout << "Set is empty\n";
}
// 获取元素数量
std::cout << "Size: " << wordSet.size() << std::endl;
// 获取可能的最大元素数量
std::cout << "Max size: " << wordSet.max_size() << std::endl;
6. set的高级特性与自定义排序
6.1 自定义比较函数
默认情况下,set使用std::less进行元素排序。我们可以自定义比较函数:
cpp复制// 自定义比较结构体
struct CaseInsensitiveCompare {
bool operator()(const std::string& a, const std::string& b) const {
return std::lexicographical_compare(
a.begin(), a.end(),
b.begin(), b.end(),
[](char c1, char c2) {
return tolower(c1) < tolower(c2);
});
}
};
// 使用自定义比较的set
std::set<std::string, CaseInsensitiveCompare> caseInsensitiveSet;
caseInsensitiveSet.insert("Apple");
caseInsensitiveSet.insert("banana");
caseInsensitiveSet.insert("apple"); // 不会插入,因为"Apple"已存在
std::cout << "Size: " << caseInsensitiveSet.size() << std::endl; // 输出2
6.2 set与其他容器的交互
set可以方便地与其他容器进行数据交换:
cpp复制std::set<int> setA = {1, 3, 5};
std::set<int> setB = {2, 4, 6};
// 交换内容
setA.swap(setB);
// 从vector导入数据
std::vector<int> vec = {7, 8, 9, 9, 8}; // 包含重复
setA.insert(vec.begin(), vec.end()); // 自动去重
// 导出到vector
std::vector<int> uniqueVec(setA.begin(), setA.end());
7. set的性能考量与使用场景
7.1 时间复杂度分析
了解set操作的时间复杂度有助于合理使用:
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| insert | O(log n) | 插入单个元素 |
| erase | O(log n) | 删除单个元素 |
| find | O(log n) | 查找元素 |
| lower_bound | O(log n) | 找到第一个不小于key的元素 |
| upper_bound | O(log n) | 找到第一个大于key的元素 |
| begin | O(1) | 获取最小元素 |
| rbegin | O(1) | 获取最大元素 |
7.2 典型使用场景
set特别适合以下场景:
- 需要维护一个唯一元素集合
- 需要频繁查询元素是否存在
- 需要按顺序遍历元素
- 需要快速找到某个范围内的元素
- 需要获取集合中的最小或最大元素
例如,在实现一个单词统计程序时,可以用set来存储所有唯一的单词:
cpp复制std::set<std::string> uniqueWords;
std::string word;
while(std::cin >> word) {
uniqueWords.insert(word);
}
std::cout << "Unique word count: " << uniqueWords.size() << std::endl;
8. set的常见问题与解决方案
8.1 自定义类型的set使用
当set存储自定义类型时,需要提供比较方法:
cpp复制struct Person {
std::string name;
int age;
};
// 方法1:重载operator<
bool operator<(const Person& a, const Person& b) {
return a.age < b.age; // 按年龄排序
}
std::set<Person> personSet;
// 方法2:使用自定义比较函数对象
struct PersonCompare {
bool operator()(const Person& a, const Person& b) const {
return a.name < b.name; // 按姓名排序
}
};
std::set<Person, PersonCompare> personByNameSet;
8.2 处理set的迭代器失效
set的迭代器在元素删除时可能会失效:
cpp复制std::set<int> nums = {1, 2, 3, 4, 5};
// 错误:删除元素后继续使用迭代器
for(auto it = nums.begin(); it != nums.end(); ++it) {
if(*it % 2 == 0) {
nums.erase(it); // 错误!it失效后继续++
}
}
// 正确做法1:使用返回值
for(auto it = nums.begin(); it != nums.end(); ) {
if(*it % 2 == 0) {
it = nums.erase(it); // erase返回下一个有效迭代器
} else {
++it;
}
}
// 正确做法2:C++11起使用erase_if算法
std::erase_if(nums, [](int n) { return n % 2 == 0; });
8.3 性能优化技巧
- 对于已知插入位置的元素,使用hint版本的insert:
cpp复制std::set<int> s;
auto hint = s.end();
for(int i = 1000; i > 0; --i) {
hint = s.insert(hint, i); // 提供正确hint可提升性能
}
-
批量插入时,使用范围insert比单元素insert更高效。
-
考虑使用unordered_set当不需要有序性时。
-
对于小型集合,vector+sort+unique可能比set更高效。
