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 底层:红黑树(平衡二叉搜索树) 核心特性:自动排序 · 唯一性(set) / 可重复(multiset) 自定义排序:仿函数 / lambda / 函数指针 对比 unordered_set:有序 vs 无序 · O(log n) vs O(1) 实际应用:去重排序 · 区间查询 · 频率统计 有序容器 红黑树实现 C++ STL 关联容器

五、总结与建议

set和multiset是STL里被低估的容器。很多人觉得它们功能单一,其实不然:

  • 自定义排序让set能适应各种业务逻辑
  • lower_bound/upper_bound组合能高效处理区间查询
  • multiset在处理重复数据时比手动维护vector+排序优雅得多

最后给个建议:如果你的数据量在十万级以下,set的O(log n)性能完全够用,而且代码更简洁。只有当你需要百万级以上的查找性能时,才值得去折腾unordered_set的hash函数。

嗯,set的高级用法就聊到这里。下次遇到需要排序+去重的场景,不妨试试set,说不定会有惊喜。


公众号:蓝海资料掘金营,微信deep3321