set/multiset高级应用:自定义排序规则、set与unordered_set对比、set实际应用
说实话,很多C++开发者对set的理解就停留在“自动排序”和“去重”这两个功能上。但实际项目中,set的玩法远不止这些。今天我就带你深入看看set和multiset的高级用法,包括自定义排序、与unordered_set的对比,以及一些实战场景。
一、自定义排序规则:让set按你的想法来
默认情况下,set使用std::less对元素进行升序排列。但有时候我们需要降序,或者按照某个成员变量排序。这时候就需要自定义比较器了。
核心要点:set的比较器必须满足严格弱序(strict weak ordering)——即不能同时出现a
1. 使用函数对象(仿函数)
#include <set>
#include <string>
#include <iostream>
struct Person {
std::string name;
int age;
};
// 自定义比较器:按年龄降序
struct CompareByAgeDesc {
bool operator()(const Person& a, const Person& b) const {
return a.age > b.age; // 年龄大的排前面
}
};
int main() {
std::set<Person, CompareByAgeDesc> people;
people.insert({"Alice", 30});
people.insert({"Bob", 25});
people.insert({"Charlie", 35});
for (const auto& p : people) {
std::cout << p.name << " (" << p.age << ")\n";
}
// 输出:Charlie (35), Alice (30), Bob (25)
return 0;
}
我在项目中遇到过类似场景:需要按用户最后登录时间降序排列,同时还要去重。用set配合自定义比较器,一行代码就搞定了排序逻辑,比手动维护vector+sort省心多了。
2. 使用lambda表达式(C++11起)
auto cmp = [](int a, int b) { return a > b; };
std::set<int, decltype(cmp)> s(cmp);
s.insert({3, 1, 4, 1, 5, 9});
for (int x : s) std::cout << x << " "; // 输出:9 5 4 3 1
小技巧:用lambda时记得用decltype(cmp)声明比较器类型,并且构造函数要传入lambda对象。我刚开始用的时候老忘记传参,结果编译报错一脸懵。
二、set与unordered_set对比:选谁?
很多新手会纠结:到底用set还是unordered_set?说白了,这就是有序vs无序、红黑树vs哈希表的对决。
| 特性 | set | unordered_set |
|---|---|---|
| 底层结构 | 红黑树(平衡二叉搜索树) | 哈希表 |
| 元素顺序 | 自动排序(升序) | 无序 |
| 插入/删除/查找复杂度 | O(log n) | 平均O(1),最坏O(n) |
| 内存占用 | 较低(每个节点存左右指针+颜色) | 较高(需要维护桶和哈希表) |
| 迭代器稳定性 | 插入/删除不影响其他迭代器 | rehash时所有迭代器失效 |
| 自定义类型支持 | 需要重载operator< 或提供比较器 | 需要提供hash函数和operator== |
我个人习惯这样选:
- 需要有序遍历(比如按时间顺序展示)→ 用set
- 追求极致查找速度(比如百万级数据查重)→ 用unordered_set
- 内存敏感(嵌入式环境)→ 用set
- 自定义类型复杂(写hash函数太麻烦)→ 用set
避坑指南:我曾经在项目中用unordered_set存自定义类型,结果忘了提供hash函数特化,编译过了但运行时疯狂碰撞,性能比set还差。后来老老实实换了set,反而更省心。
三、set的实际应用场景
光讲理论没意思,来看看set在真实项目中怎么用。
场景1:数据去重+排序
#include <set>
#include <vector>
#include <algorithm>
std::vector<int> removeDuplicatesAndSort(const std::vector<int>& input) {
std::set<int> s(input.begin(), input.end());
return std::vector<int>(s.begin(), s.end());
}
// 使用
std::vector<int> data = {5, 2, 8, 2, 5, 1, 9, 1};
auto result = removeDuplicatesAndSort(data);
// result: {1, 2, 5, 8, 9}
你看,三行代码搞定去重+排序。要是自己写循环,至少得七八行,还容易出错。
场景2:区间查询(lower_bound/upper_bound)
std::set<int> s = {1, 3, 5, 7, 9, 11};
// 查找[4, 8]区间内的元素
auto it_low = s.lower_bound(4); // 第一个 >= 4 的元素 → 5
auto it_up = s.upper_bound(8); // 第一个 > 8 的元素 → 9
for (auto it = it_low; it != it_up; ++it) {
std::cout << *it << " "; // 输出:5 7
}
这个特性在日程管理、IP地址范围查询等场景特别有用。我记得有一次做日志分析系统,需要按时间戳范围筛选记录,用set的lower_bound/upper_bound组合,性能比遍历vector快了不止一个数量级。
场景3:multiset处理重复数据
#include <set>
std::multiset<int> ms;
ms.insert(5);
ms.insert(3);
ms.insert(5); // 允许重复
ms.insert(1);
ms.insert(3);
// 统计某个值的出现次数
auto count = ms.count(5); // 2
// 删除所有等于3的元素
ms.erase(3); // 删除所有3
// 删除第一个等于5的元素
auto it = ms.find(5);
if (it != ms.end()) ms.erase(it);
注意:multiset的erase(value)会删除所有匹配的元素。如果只想删一个,先用find找到迭代器,再erase迭代器。这个坑我踩过——本来只想删一个,结果全没了。
四、知识体系总览
下面这张图帮你理清set/multiset的核心脉络:
五、总结与建议
set和multiset是STL里被低估的容器。很多人觉得它们功能单一,其实不然:
- 自定义排序让set能适应各种业务逻辑
- lower_bound/upper_bound组合能高效处理区间查询
- multiset在处理重复数据时比手动维护vector+排序优雅得多
最后给个建议:如果你的数据量在十万级以下,set的O(log n)性能完全够用,而且代码更简洁。只有当你需要百万级以上的查找性能时,才值得去折腾unordered_set的hash函数。
嗯,set的高级用法就聊到这里。下次遇到需要排序+去重的场景,不妨试试set,说不定会有惊喜。