6. set/multiset容器:从入门到实战

set和multiset,说白了就是C++里的「集合」。我刚开始学STL时,觉得这玩意儿不就是个数组吗?后来才发现,它背后的设计哲学其实挺讲究的。

6.1 set的基本概念

set是一个自动排序元素唯一的容器。它底层用红黑树实现,插入、删除、查找的时间复杂度都是O(log n)。

我个人习惯把set理解成「有序的数学集合」。你想想看,数学里的集合有什么特点?元素不重复、没有顺序——但STL的set额外帮你排好序了。

核心特性:

  • 所有元素在插入时自动排序(默认升序)
  • 元素值不可修改(因为修改会破坏排序结构)
  • 查找效率高,适合做去重和快速检索

我在项目中遇到过这样一个场景:需要统计用户访问的IP地址,并且要按字典序输出。用set简直不要太爽——插入时自动去重+排序,一行代码搞定。

6.2 set的构造与赋值

构造set的方式有几种,我直接上代码:

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

void test() {
    // 默认构造
    set<int> s1;
    
    // 初始化列表构造(C++11起)
    set<int> s2 = {5, 3, 8, 1, 3, 5};  // 实际存储:1, 3, 5, 8
    
    // 拷贝构造
    set<int> s3(s2);
    
    // 迭代器区间构造
    int arr[] = {9, 2, 7, 4};
    set<int> s4(arr, arr + 4);  // 存储:2, 4, 7, 9
    
    // 赋值操作
    set<int> s5;
    s5 = s2;
}

嗯,这里要注意:set的构造函数会自动去重。你看s2里我塞了两个3和两个5,实际只存了一份。

6.3 set的大小与交换

这部分比较简单,我快速过一下:

void test_size_swap() {
    set<int> s = {1, 2, 3, 4, 5};
    
    // 大小相关
    cout << "size: " << s.size() << endl;      // 5
    cout << "empty: " << s.empty() << endl;    // 0(非空)
    cout << "max_size: " << s.max_size() << endl; // 理论最大容量
    
    // 交换
    set<int> s2 = {10, 20};
    s.swap(s2);  // 现在s存{10,20},s2存{1,2,3,4,5}
}

swap操作是O(1)的,因为它只交换内部指针。我曾经在需要频繁切换两个集合的场景下用过swap,性能很好。

6.4 set的插入与删除

插入和删除是set最常用的操作。我直接上实战代码:

void test_insert_erase() {
    set<int> s;
    
    // 插入方式1:insert返回pair<iterator, bool>
    pair<set<int>::iterator, bool> ret = s.insert(10);
    if (ret.second) {
        cout << "插入成功" << endl;
    }
    
    // 插入方式2:重复插入会失败
    ret = s.insert(10);  // 第二次插入,second为false
    if (!ret.second) {
        cout << "元素已存在,插入失败" << endl;
    }
    
    // 批量插入
    s.insert({20, 30, 40});
    
    // 删除
    s.erase(20);           // 按值删除
    s.erase(s.begin());    // 按迭代器删除
    s.erase(s.begin(), s.end());  // 清空区间
    s.clear();             // 清空所有元素
}

我曾经踩过的坑:用erase(iterator)删除元素后,这个迭代器就失效了。如果你在循环中删除,一定要用返回值获取下一个有效迭代器:

// 正确做法
for (auto it = s.begin(); it != s.end(); ) {
    if (*it % 2 == 0) {
        it = s.erase(it);  // erase返回下一个元素的迭代器
    } else {
        ++it;
    }
}

6.5 set的查找与统计

查找是set的强项。红黑树结构让查找变得非常快:

void test_find_count() {
    set<int> s = {1, 2, 3, 4, 5};
    
    // find:找到返回迭代器,找不到返回end()
    auto it = s.find(3);
    if (it != s.end()) {
        cout << "找到了: " << *it << endl;
    }
    
    // count:统计元素个数(set中要么0要么1)
    int cnt = s.count(3);  // 返回1
    cnt = s.count(10);     // 返回0
    
    // lower_bound / upper_bound
    auto low = s.lower_bound(3);   // 第一个>=3的元素
    auto up = s.upper_bound(3);    // 第一个>3的元素
    cout << "lower_bound: " << *low << endl;  // 3
    cout << "upper_bound: " << *up << endl;   // 4
}

为什么count在set里只有0或1?因为set不允许重复。但multiset就不一样了——这也是我们接下来要说的。

6.6 set与multiset的区别

特性 set multiset
元素唯一性 不允许重复 允许重复
插入重复元素 插入失败 插入成功
count()返回值 0或1 实际重复次数
典型应用 去重、集合运算 统计频率、多值排序
void test_multiset() {
    multiset<int> ms;
    ms.insert(10);
    ms.insert(10);  // 可以重复插入
    ms.insert(20);
    
    cout << "count(10): " << ms.count(10) << endl;  // 2
    
    // 查找时注意:find返回第一个遇到的元素
    auto it = ms.find(10);
    // 如果要删除所有10
    ms.erase(10);  // 删除所有值为10的元素
}

我的建议:如果你不确定是否需要重复元素,先用set。因为从set改成multiset很容易,反过来却可能破坏已有逻辑。

6.7 pair对组创建

pair在set中随处可见——insert的返回值就是pair。它其实就是把两个值打包在一起:

void test_pair() {
    // 创建方式1:直接构造
    pair<string, int> p1("age", 25);
    cout << p1.first << ": " << p1.second << endl;
    
    // 创建方式2:make_pair
    auto p2 = make_pair("score", 98);
    
    // 创建方式3:拷贝
    pair<string, int> p3(p1);
    
    // 实际应用:set的insert返回值
    set<int> s;
    pair<set<int>::iterator, bool> ret = s.insert(42);
    if (ret.second) {
        cout << "插入成功,元素值: " << *(ret.first) << endl;
    }
}

pair说白了就是个「二合一」的容器。我在写图算法时经常用pair来存边的起点和终点,比定义结构体省事多了。

知识体系总览

下面这张图帮你理清set/multiset的核心脉络:

set / multiset set(唯一元素) 自动排序 + 去重 insert返回pair<iter,bool> count() 返回 0 或 1 multiset(可重复) 自动排序 + 允许重复 insert总是成功 count() 返回实际个数 共同操作:构造 / 赋值 / 大小 / 交换 / 查找

set和multiset就像一对双胞胎——长得像,但性格不同。set严谨,不允许重复;multiset随和,来者不拒。选哪个?看你的业务需求。

我个人更常用set,因为去重是刚需。但如果你在做词频统计之类的任务,multiset就派上用场了。


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