1. set容器基础概念与特性解析
set是C++标准模板库(STL)中的关联式容器,底层通常采用红黑树实现。与vector、list等序列式容器不同,set具有以下核心特性:
- 自动排序:元素插入后自动按升序排列(默认使用
less<Key>比较) - 唯一性保证:容器内不允许存在相同键值的元素
- 高效查找:基于红黑树的实现使得查找时间复杂度为O(log n)
cpp复制#include <set>
using namespace std;
set<int> mySet; // 声明一个整型set
关键细节:set的迭代器属于双向迭代器,不支持随机访问(不能使用[]运算符),但支持++/--操作
2. set核心操作全解析
2.1 基础操作
cpp复制// 插入元素(自动去重)
mySet.insert(3);
mySet.insert(1);
mySet.insert(4);
// 遍历输出(已自动排序)
for(auto it = mySet.begin(); it != mySet.end(); ++it) {
cout << *it << " "; // 输出:1 3 4
}
// 查找元素
auto pos = mySet.find(3);
if(pos != mySet.end()) {
cout << "Found: " << *pos << endl;
}
// 删除元素
mySet.erase(4); // 通过值删除
mySet.erase(pos); // 通过迭代器删除
2.2 高级操作
cpp复制// 获取元素数量
cout << mySet.size();
// 检查是否为空
if(mySet.empty()) {...}
// 清空容器
mySet.clear();
// 边界查找
auto lower = mySet.lower_bound(2); // 第一个不小于2的元素
auto upper = mySet.upper_bound(3); // 第一个大于3的元素
3. set在算法题中的实战应用
3.1 去重与排序问题
例题:给定数组[4,2,3,2,5,4],输出去重后的升序排列结果
cpp复制vector<int> nums = {4,2,3,2,5,4};
set<int> uniqueSet(nums.begin(), nums.end());
vector<int> result(uniqueSet.begin(), uniqueSet.end());
// result: [2,3,4,5]
3.2 范围查询问题
例题:统计考试成绩在[60,90]区间内的学生数量
cpp复制set<int> scores = {55,78,92,63,84,71};
auto low = scores.lower_bound(60);
auto high = scores.upper_bound(90);
cout << distance(low, high); // 输出4
3.3 滑动窗口最大值问题
利用set维护窗口内的有序元素:
cpp复制vector<int> maxSlidingWindow(vector<int>& nums, int k) {
set<int> window;
vector<int> result;
for(int i = 0; i < nums.size(); ++i) {
window.insert(nums[i]);
if(i >= k-1) {
result.push_back(*window.rbegin()); // 获取最大值
window.erase(nums[i-k+1]); // 移除最左边元素
}
}
return result;
}
4. set的变体:multiset与unordered_set
4.1 multiset(允许重复元素)
cpp复制multiset<int> ms;
ms.insert(1);
ms.insert(1); // 允许重复
cout << ms.count(1); // 输出2
4.2 unordered_set(哈希实现)
cpp复制unordered_set<int> us;
us.insert(3);
us.insert(1); // 无序存储
性能对比:unordered_set查找为O(1),但不保持元素顺序
5. set的底层实现与性能优化
5.1 红黑树特性
- 每个节点非红即黑
- 根节点是黑色
- 红色节点的子节点必须是黑色
- 从任一节点到其叶子的所有路径包含相同数目的黑色节点
5.2 自定义比较函数
cpp复制struct MyCompare {
bool operator()(const string& a, const string& b) const {
return a.length() < b.length();
}
};
set<string, MyCompare> lengthSet;
6. 高频面试题实战解析
6.1 两数之和变种
题目:设计一个数据结构,支持添加数字和快速查找是否存在两个数之和等于目标值
cpp复制class TwoSum {
private:
multiset<int> nums;
public:
void add(int number) {
nums.insert(number);
}
bool find(int value) {
for(auto it = nums.begin(); it != nums.end(); ++it) {
int target = value - *it;
if((target == *it && nums.count(target) > 1) ||
(target != *it && nums.count(target) > 0)) {
return true;
}
}
return false;
}
};
6.2 最近公共祖先(LCA)问题
利用set记录路径节点:
cpp复制TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
set<TreeNode*> path;
while(p) {
path.insert(p);
p = p->parent;
}
while(q) {
if(path.count(q)) return q;
q = q->parent;
}
return nullptr;
}
7. 性能对比与使用建议
| 操作 | set | unordered_set |
|---|---|---|
| 插入 | O(log n) | O(1) |
| 删除 | O(log n) | O(1) |
| 查找 | O(log n) | O(1) |
| 有序遍历 | 支持 | 不支持 |
| 内存占用 | 较低 | 较高 |
使用场景建议:
- 需要元素有序 → set
- 只需要判断存在性 → unordered_set
- 允许重复元素 → multiset
8. 常见问题排查
8.1 迭代器失效问题
cpp复制set<int> s = {1,2,3};
auto it = s.begin();
s.erase(it); // it失效,但返回下一个有效迭代器
it = s.erase(it); // 正确写法
8.2 自定义类型作为key
必须重载<运算符或提供比较函数:
cpp复制struct Person {
string name;
int age;
bool operator<(const Person& other) const {
return age < other.age;
}
};
set<Person> personSet;
9. 扩展应用:与其他容器配合使用
9.1 与vector转换
cpp复制set<int> s = {3,1,4};
vector<int> v(s.begin(), s.end()); // 已排序
9.2 与map配合
cpp复制map<int, set<string>> categoryMap;
categoryMap[1].insert("apple");
categoryMap[1].insert("banana");
10. 实际工程中的注意事项
- 内存碎片:频繁插入删除可能导致内存碎片,必要时考虑
unordered_set - 线程安全:STL容器非线程安全,多线程环境需要加锁
- 性能监控:对于超大规模数据,建议监控红黑树深度
cpp复制// 线程安全示例
mutex mtx;
set<int> safeSet;
void threadFunc(int val) {
lock_guard<mutex> lock(mtx);
safeSet.insert(val);
}
11. 性能测试与优化案例
测试100万次插入操作:
cpp复制set<int> s;
auto start = chrono::high_resolution_clock::now();
for(int i = 0; i < 1000000; ++i) {
s.insert(rand());
}
auto end = chrono::high_resolution_clock::now();
cout << chrono::duration_cast<chrono::milliseconds>(end-start).count() << "ms";
优化建议:
- 预分配内存(通过reserve对unordered_set有效)
- 批量插入使用
insert(first, last) - 对于已知范围的整数,考虑位图替代
12. C++17/20新特性应用
12.1 节点操作(C++17)
cpp复制set<int> s1 = {1,2,3}, s2;
auto node = s1.extract(2); // 提取节点
s2.insert(move(node)); // 转移节点
12.2 合并操作(C++17)
cpp复制set<int> s1 = {1,2}, s2 = {2,3};
s1.merge(s2); // s1: {1,2,3}, s2: {2}
13. 综合实战:设计排行榜系统
cpp复制class Leaderboard {
private:
map<int, set<int>> scoreMap; // 分数到玩家ID的映射
unordered_map<int, int> playerScores; // 玩家ID到分数的映射
public:
void addScore(int playerId, int score) {
if(playerScores.count(playerId)) {
int oldScore = playerScores[playerId];
scoreMap[oldScore].erase(playerId);
if(scoreMap[oldScore].empty()) {
scoreMap.erase(oldScore);
}
}
playerScores[playerId] += score;
scoreMap[playerScores[playerId]].insert(playerId);
}
int top(int K) {
int sum = 0;
for(auto it = scoreMap.rbegin(); it != scoreMap.rend() && K > 0; ++it) {
int count = min(K, (int)it->second.size());
sum += it->first * count;
K -= count;
}
return sum;
}
void reset(int playerId) {
int score = playerScores[playerId];
scoreMap[score].erase(playerId);
if(scoreMap[score].empty()) {
scoreMap.erase(score);
}
playerScores.erase(playerId);
}
};
14. 与其他语言相似结构的对比
| 特性 | C++ set | Java TreeSet | Python set |
|---|---|---|---|
| 底层实现 | 红黑树 | 红黑树 | 哈希表 |
| 有序性 | 是 | 是 | 否 |
| 线程安全 | 否 | 部分方法同步 | 非线程安全 |
| 时间复杂度 | 插入O(log n) | 插入O(log n) | 插入O(1) |
15. 经典算法题精讲
15.1 会议室II问题
题目:给定会议时间区间,求最少需要多少会议室
cpp复制int minMeetingRooms(vector<vector<int>>& intervals) {
map<int, int> timeMap; // 时间点->变化量
for(auto& interval : intervals) {
timeMap[interval[0]]++;
timeMap[interval[1]]--;
}
int rooms = 0, maxRooms = 0;
for(auto& [time, delta] : timeMap) {
rooms += delta;
maxRooms = max(maxRooms, rooms);
}
return maxRooms;
}
15.2 数据流的中位数
cpp复制class MedianFinder {
private:
multiset<int> data;
multiset<int>::iterator mid;
public:
MedianFinder() : mid(data.end()) {}
void addNum(int num) {
data.insert(num);
if(data.size() == 1) {
mid = data.begin();
} else if(num < *mid) {
mid = (data.size() % 2 == 0) ? mid : prev(mid);
} else {
mid = (data.size() % 2 == 0) ? next(mid) : mid;
}
}
double findMedian() {
if(data.size() % 2 == 0) {
return (*mid + *next(mid)) / 2.0;
} else {
return *mid;
}
}
};
16. 内存管理与性能陷阱
- 节点内存开销:每个set元素需要额外存储颜色标记和指针
- 缓存不友好:红黑树节点可能分散在内存各处
- 解决方案:
- 对小数据集,考虑连续存储结构
- 对频繁访问的数据,可配合缓存使用
cpp复制// 内存占用测试示例
set<int> s;
cout << "Empty set size: " << sizeof(s) << endl;
for(int i = 0; i < 100; ++i) s.insert(i);
cout << "100 elements size: " << sizeof(s) + sizeof(int)*100 << endl;
17. 设计模式中的应用
17.1 观察者模式
使用set存储观察者:
cpp复制class Subject {
private:
set<Observer*> observers;
public:
void attach(Observer* obs) {
observers.insert(obs);
}
void detach(Observer* obs) {
observers.erase(obs);
}
void notify() {
for(auto obs : observers) {
obs->update();
}
}
};
17.2 享元模式
管理共享对象:
cpp复制class FlyweightFactory {
private:
set<shared_ptr<Flyweight>> pool;
public:
shared_ptr<Flyweight> getFlyweight(const string& key) {
auto it = find_if(pool.begin(), pool.end(),
[&](auto ptr){ return ptr->key() == key; });
if(it != pool.end()) return *it;
auto newObj = make_shared<ConcreteFlyweight>(key);
pool.insert(newObj);
return newObj;
}
};
18. 跨平台开发注意事项
- 实现差异:不同编译器的红黑树实现可能有细微差别
- 调试技巧:
- GCC可使用
_GLIBCXX_DEBUG宏检测迭代器错误 - Visual Studio有迭代器检查选项
- GCC可使用
- ABI兼容性:不同版本STL可能不兼容
cpp复制// 调试模式示例(GCC)
#define _GLIBCXX_DEBUG
#include <set>
// 会进行额外的迭代器有效性检查
19. 替代方案与扩展阅读
- boost::flat_set:基于数组的实现,缓存友好
- B-tree容器:适合磁盘存储场景
- 跳表实现:Redis等系统常用
推荐阅读:
- 《STL源码剖析》中红黑树实现章节
- C++标准文档中关于关联容器的要求
- 各编译器(STL)的实现差异分析
20. 最佳实践总结
-
选择原则:
- 需要有序 → set
- 只需要存在性检查 → unordered_set
- 需要频繁合并/分割 → C++17的节点操作
-
性能关键点:
- 避免在循环中频繁查找
- 批量操作优于单次操作
- 自定义比较函数尽量简单
-
代码可读性:
- 对复杂比较函数添加注释
- 使用类型别名简化声明
cpp复制// 类型别名示例
using EmployeeSet = set<Employee, function<bool(const Employee&, const Employee&)>>;
EmployeeSet es([](auto& a, auto& b){ return a.id < b.id; });
在实际工程中,set容器特别适合需要维护有序唯一元素的场景。我在多个分布式系统项目中用它来管理配置项、维护会话ID列表等,其自动排序特性大幅简化了业务逻辑。一个特别有用的技巧是结合lower_bound和upper_bound实现范围查询,这在处理时间区间或数值范围时非常高效。
