12. set与multiset容器:set的构造与操作、set的自动排序、set的查找与统计、multiset与set的区别

好,咱们今天来聊聊 set 和 multiset。这两个容器,说白了就是「有序集合」和「有序多重集合」。我当年刚接触 STL 时,觉得 set 不就是个数组嘛,有啥特别的?后来在项目里被坑过一次,才真正理解了它的价值。

set 的核心特点就两个:自动排序元素唯一。你往里面丢数据,它自己就排好序了,重复的还会被自动扔掉。multiset 呢,允许重复,但同样自动排序。

一句话总结: set 是「排好序且不重复的集合」,multiset 是「排好序且允许重复的集合」。

12.1 set 的构造与基本操作

set 的构造方式挺灵活的。我个人习惯用默认构造,然后一个个 insert。当然,你也可以用初始化列表,或者从其他容器拷贝。

#include <iostream>
#include <set>
using namespace std;

int main() {
    // 默认构造
    set<int> s1;
    s1.insert(5);
    s1.insert(3);
    s1.insert(8);
    s1.insert(1);
    s1.insert(3);  // 重复,会被忽略

    // 初始化列表构造(C++11 起)
    set<int> s2 = {4, 2, 7, 2, 9};

    // 拷贝构造
    set<int> s3(s2);

    // 遍历
    for (int val : s1) {
        cout << val << " ";  // 输出:1 3 5 8
    }
    cout << endl;

    return 0;
}

你看,插入顺序是 5、3、8、1、3,但输出却是 1、3、5、8。这就是自动排序的效果。重复的 3 只保留了一个。

小技巧: set 的 insert 返回一个 pair<iterator, bool>,第二个 bool 表示插入是否成功。你可以用它来判断元素是否已存在。

12.2 set 的自动排序机制

set 底层用的是红黑树(一种平衡二叉搜索树)。每次插入元素,都会触发一次 O(log n) 的查找和插入操作,同时保持树平衡。

默认情况下,set 使用 std::less<T> 作为比较函数,也就是升序排列。如果你想降序,可以指定 greater<T>

// 降序排列的 set
set<int, greater<int>> s = {3, 1, 4, 1, 5, 9};
for (int v : s) cout << v << " ";  // 输出:9 5 4 3 1

嗯,这里要注意:比较函数必须满足「严格弱序」条件。说白了就是:不能同时出现 a < b 和 b < a 都为 true 的情况。我曾经在自定义类型时踩过这个坑,比较函数写反了,结果 set 直接崩溃。

避坑指南: 自定义类型放入 set 时,必须重载 operator< 或者提供自定义比较函数。否则编译会报错。我曾经图省事没写,结果排查了半天才发现是这个问题。

12.3 set 的查找与统计

set 的查找效率很高,O(log n)。常用的查找方法有:

  • find(key):返回迭代器,找不到返回 end()
  • count(key):返回元素个数(set 中只能是 0 或 1)
  • lower_bound(key):返回第一个 >= key 的迭代器
  • upper_bound(key):返回第一个 > key 的迭代器
  • equal_range(key):返回 pair,包含 lower_bound 和 upper_bound
set<int> s = {10, 20, 30, 40, 50};

// 查找
auto it = s.find(30);
if (it != s.end()) {
    cout << "找到了: " << *it << endl;  // 30
}

// 统计
cout << "30 的个数: " << s.count(30) << endl;  // 1
cout << "35 的个数: " << s.count(35) << endl;  // 0

// 范围查找
auto low = s.lower_bound(25);   // 指向 30
auto up = s.upper_bound(35);    // 指向 40
for (auto it = low; it != up; ++it) {
    cout << *it << " ";  // 输出:30
}

你想想看,如果要在 vector 里做同样的事,你得先排序,再二分查找,麻烦多了。set 把这些都封装好了,直接用就行。

12.4 multiset 与 set 的区别

multiset 和 set 的接口几乎一模一样。唯一的区别就是:multiset 允许重复元素

特性 set multiset
元素唯一性
自动排序
insert 返回值 pair<iterator, bool> iterator(直接返回插入位置)
count(key) 返回值 0 或 1 0 到 n
erase(key) 行为 删除一个元素,返回 0 或 1 删除所有等于 key 的元素,返回删除个数
multiset<int> ms = {1, 2, 2, 3, 3, 3, 4};

cout << "2 的个数: " << ms.count(2) << endl;  // 2
cout << "3 的个数: " << ms.count(3) << endl;  // 3

// 删除所有等于 2 的元素
int removed = ms.erase(2);
cout << "删除了 " << removed << " 个元素" << endl;  // 2

// 只删除一个等于 3 的元素
auto it = ms.find(3);
if (it != ms.end()) {
    ms.erase(it);
}
cout << "剩余 3 的个数: " << ms.count(3) << endl;  // 2

实际场景: 我在做日志分析系统时,用 multiset 来存储所有请求的响应时间。因为同一个响应时间可能出现多次,用 multiset 天然支持重复,而且自动排序后,取中位数、百分位数都非常方便。

12.5 知识体系总览

下面这张图把 set 和 multiset 的核心知识点串起来了,方便你整体把握。

set 与 multiset 知识体系 set / multiset 构造与基本操作 自动排序机制 查找与统计 set vs multiset 默认构造 初始化列表 拷贝构造 默认升序 (less) 指定降序 (greater) 自定义比较函数 find() count() lower_bound / upper_bound 元素唯一性 insert 返回值不同 erase / count 行为 底层实现:红黑树(平衡二叉搜索树)

set 和 multiset 在实际项目中用得非常多。比如去重、排序、快速查找、范围查询等场景,它们都是很好的选择。我个人建议:需要唯一性就用 set,需要重复就用 multiset,别搞混了。

性能提示: set 的插入、删除、查找都是 O(log n)。如果你只需要「不重复」而不需要「排序」,可以考虑 unordered_set(哈希表实现,平均 O(1))。但 unordered_set 不保证顺序,而且内存开销更大。

好了,set 和 multiset 的核心内容就这些。记住它们的特性,写代码时多想想「我需要排序吗?」「我需要重复吗?」,选对容器,事半功倍。