set学习总结

set 学习总结

std::set 是 C++ 标准库中最常用的有序关联容器之一,本文从零到熟练梳理它的所有核心用法。

1. set 是什么

  • 存储不重复的元素,每个元素唯一。
  • 底层是红黑树(平衡二叉搜索树),插入、查找、删除都是 O(log n)。
  • 自动按元素排序(默认升序),遍历时天然有序。
  • 元素是 const 的(不能通过迭代器修改),因为修改会破坏树的有序性。
  • 属于"有序关联容器",头文件 <set>。
#include <set>

set<int> s; // 空 set,int 类型

对比记忆:set 是"只存 key 的 map"(set<T> 等价于 map<T, void>),存储唯一元素并自动排序。map 存键值对,set 只存值。

2. 定义与初始化

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

// 1. 空 set
set<int> s1;

// 2. 初始化列表
set<int> s2{3, 1, 4, 1, 5, 9}; // 自动去重且排序:{1, 3, 4, 5, 9}

// 3. 改变排序方式,降序排列
set<int, greater<int>> s3{3, 1, 4}; // {4, 3, 1}

// 4. 从其他容器构造
vector<int> v{5, 3, 1, 2, 4};
set<int> s4(v.begin(), v.end()); // {1, 2, 3, 4, 5}

// 5. 拷贝构造
set<int> s5(s2);

注意:初始化列表中重复的元素会被自动去除(如 {3, 1, 4, 1, 5} 中的两个 1 只保留一个)。

3. 核心操作一览

操作 写法 说明 复杂度
插入 s.insert(x) 元素已存在则不插入,返回 pair<iterator, bool> O(log n)
插入 s.emplace(args...) C++11,原地构造 O(log n)
查找 s.find(x) 返回迭代器,未找到返回 end() O(log n)
计数 s.count(x) 返回 0 或 1(判断是否存在) O(log n)
判断存在 s.contains(x) C++20,直接返回 bool O(log n)
删除 s.erase(x) 按值删除,返回删除个数 O(log n)
删除 s.erase(it) 按迭代器删除,返回下一个迭代器 O(1) 均摊
二分查找 s.lower_bound(x) 第一个 >= x 的迭代器 O(log n)
二分查找 s.upper_bound(x) 第一个 > x 的迭代器 O(log n)
二分查找 s.equal_range(x) 返回 pair(lower_bound, upper_bound) O(log n)
遍历 for (auto& x : s) C++11 范围 for O(n)
大小 s.size() 元素个数 O(1)
清空 s.clear() 删除所有元素 O(n)

4. 增删查改实战

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

int main() {
    set<int> s;

    // ===== 插入 =====
    auto ret = s.insert(10);
    cout << ret.second << endl; // 1,插入成功
    ret = s.insert(10);
    cout << ret.second << endl; // 0,已存在,插入失败
    s.insert(20);
    s.insert(30);
    s.emplace(40); // 原地构造

    // ===== 查找 =====
    auto it = s.find(20);
    if (it != s.end()) {
        cout << "Found: " << *it << endl; // Found: 20
    }
    cout << s.count(30) << endl; // 1
    // s.contains(30) // C++20,等价于 count != 0

    // ===== 遍历(默认升序) =====
    for (auto& x : s) {
        cout << x << " "; // 10 20 30 40
    }
    cout << endl;

    // ===== 降序遍历 =====
    set<int, greater<int>> s2(s.begin(), s.end());
    for (auto& x : s2) {
        cout << x << " "; // 40 30 20 10
    }
    cout << endl;

    // ===== 删除 =====
    s.erase(20);     // 按值删除,返回 1
    s.erase(s.begin()); // 按迭代器删除
    s.clear();
    cout << s.size() << endl; // 0

    return 0;
}

5. set 的"去重+排序"特性(重点)

set 最大的特点就是自动去重且排序,这在很多场景下非常方便:

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

int main() {
    // 场景:从乱序有重复的数据中提取不重复且有序的元素
    vector<int> v{5, 3, 1, 4, 1, 5, 9, 2, 6, 5, 3};
    set<int> s(v.begin(), v.end());

    for (auto& x : s) {
        cout << x << " "; // 1 2 3 4 5 6 9
    }
    cout << endl;

    return 0;
}

坑:set 的元素是 const 的,不能通过迭代器修改!

set<int> s{1, 2, 3};
auto it = s.begin();
// *it = 10; // 编译错误!元素是 const

// 想修改元素:先删后插
s.erase(it);
s.insert(10); // OK

6. 排序:set 的排序由比较器决定

set 在插入时就按元素排好序,之后不能动态改。排序规则在创建时通过第二个模板参数指定:

#include <set>
#include <functional>
#include <string>
using namespace std;

// 默认:升序
set<int> s1;

// 降序
set<int, greater<int>> s2;

// 自定义比较器(函数对象)
struct Cmp {
    bool operator()(const string& a, const string& b) const {
        return a.size() < b.size(); // 按字符串长度排序
    }
};
set<string, Cmp> s3;
s3.insert("hello");
s3.insert("hi");     // "hi" 排在 "hello" 前面(长度 2 < 5)
s3.insert("world");

// C++20:lambda 作为比较器
auto cmp = [](int a, int b) { return a > b; };
set<int, decltype(cmp)> s4(cmp);

关键限制(与 map 相同):

  1. 比较器必须是严格弱序。
  2. 创建后不能换比较器。
  3. 按非默认方式排序必须在声明时指定比较器。

7. 二分查找:lower_bound / upper_bound / equal_range

set 天然有序,可以直接二分查找:

set<int> s{1, 3, 5, 7, 9};

auto it1 = s.lower_bound(4); // 第一个 >= 4 的元素 -> 5
auto it2 = s.upper_bound(4); // 第一个 >  4 的元素 -> 5
auto it3 = s.lower_bound(5); // -> 5
auto it4 = s.upper_bound(5); // -> 7

// equal_range 同时拿到 [lower, upper)
auto [lo, hi] = s.equal_range(5); // 覆盖 == 5 的区间(对 set 只有一个元素)
for (auto it = lo; it != hi; ++it) {
    cout << *it << endl; // 5
}

小技巧:equal_range 配合 multiset 才是真正的"批量取相同元素"场景。

8. 迭代器的特点

  • set 的迭代器是双向迭代器:只支持 ++ / --,不支持 it + n(vector 才可以)。
  • 迭代器指向的元素是 const 的,不能修改。
  • 删除元素只会使指向被删元素的迭代器失效,其它迭代器不受影响,所以可以边遍历边删:
set<int> s{1, 2, 3, 4, 5};
for (auto it = s.begin(); it != s.end();) {
    if (*it % 2 == 0) it = s.erase(it); // 删除偶数,erase 返回下一个迭代器
    else ++it;
}
// s: {1, 3, 5}

9. set 与 unordered_set、multiset 的对比

set unordered_set multiset
底层 红黑树 哈希表 红黑树
有序 按元素有序 无序 按元素有序
元素重复 不允许 不允许 允许
复杂度 插入/查找/删除 O(log n) 平均 O(1) O(log n)
迭代器 双向 无序(单向) 双向
适用场景 需要有序、范围查询 只求查找速度 需要统计重复次数
#include <unordered_set>
unordered_set<int> us; // 查找最快,但遍历顺序不确定

#include <set>
multiset<int> ms;
ms.insert(1);
ms.insert(1); // 允许重复
ms.insert(2);
cout << ms.count(1); // 2,统计重复次数

选择建议:要排序/范围查询用 set,只追求 O(1) 查找用 unordered_set。

10. 自定义类型做元素

元素必须能"比较大小",所以自定义类型要重载 operator<(满足严格弱序):

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

struct Student {
    string name;
    int score;

    bool operator<(const Student& other) const { // 必须 const
        return score < other.score; // 按分数排序
    }
};

set<Student> s;
s.insert({"Alice", 90});
s.insert({"Bob", 85});
s.insert({"Charlie", 95});
// 遍历时按 score 升序:Bob(85) -> Alice(90) -> Charlie(95)

11. set 与 map 的关系

set<T> 本质上就是 map<T, none_type>——只存 key 不存 value:

// set 的 insert
s.insert(10);

// 等价于 map 的操作
map<int, monostate> m;
m.insert({10, monostate{}});

所以 set 的所有接口(find、count、erase、lower_bound 等)与 map 的 set 部分完全一致。

multiset 对应 multimap:允许重复元素的有序关联容器。

12. 常见坑汇总

  1. [] 不存在:set 没有 operator[],查找用 find / count / contains。
  2. 元素是 const:不能通过迭代器修改元素。想"修改":先 erase 旧值,再 insert 新值。
  3. 不能用会改元素的 STL 算法:std::sort、std::remove、std::unique 等会修改元素的算法不能直接用于 set(元素是 const)。set 有自己的 sort 吗?没有!set 的排序由比较器决定,插入时自动排序。
  4. 自定义类型忘了写 operator< → 编译报错。
  5. 比较器不满足严格弱序 → 行为未定义。
  6. 遍历时修改元素:不行,编译器直接报错(元素是 const)。
  7. set 的迭代器是双向的,别写 it + 1。
  8. insert 不覆盖:已存在则不插入。想覆盖用 erase + insert,或用 map。
  9. clear 不释放内存:与 vector 类似,但 set 底层是树,clear 会释放节点内存。

13. 实战场景

给定一个需求:需要存储一组唯一的数据,支持快速查找、自动排序、范围查询。

方法一:使用std::vector + std::sort + std::unique建模,但插入时去重需要 O(n) 移动元素,不适合频繁增删。
方法二:使用std::set建模,插入/查找/删除都是 O(log n),自动去重且排序,支持 lower_bound/upper_bound 范围查询。
方法三:使用std::unordered_set建模,查找 O(1) 最快,但无序、不支持范围查询。

总结:
1、需要有序且去重,选择 set
2、只需要快速查找不关心顺序,选择 unordered_set(O(1) 平均)
3、需要统计重复次数,选择 multiset
4、需要按键查找+按值排序输出,选择 map 或 set + vector 组合
5、频繁检查元素是否存在:set 的 count / contains 比 vector 的 find 快得多(O(log n) vs O(n))
6、set 的迭代器是双向的,不能 it+n 跳跃访问;vector 的迭代器是随机访问的,支持 it+n
另外红黑树不是连续内存,遍历时跳内存访问速度慢,vector 连续存储遍历快

posted @ 2026-09-08 23:47  LemHou  阅读(21)  评论(0)    收藏  举报