前言
在学习 C++ STL 时,我们最开始接触的是 string、vector、list、deque 等序列式容器,它们的特点是线性结构、按存储位置访问、交换元素不破坏结构 。而今天要深入讲解的 set / multiset ,属于关联式容器 ,底层基于红黑树(平衡二叉搜索树) 实现,天生具备排序、去重、高效查找 能力,增删查效率稳定在 O(logN),是处理查找、去重、有序存储的"王牌容器"。
本文完全依据 STL 标准文档与源码设计思路,把 set 的**原理、构造、迭代器、增删查、边界查找、multiset 区别进行讲解
一、序列式容器 , 关联式容器
先把最核心的概念区分清楚:
1. 序列式容器
- 逻辑结构:线性序列
- 元素关系:松散,按位置保存访问
- 代表:vector、list、string、deque、array
- 特点:交换元素不影响容器结构,只改变顺序
2. 关联式容器
- 逻辑结构:非线性(树型)
- 元素关系:紧密,按 key(关键字) 组织
- 代表:set / multiset、map / multimap、unordered系列
- 特点:修改 key 会破坏结构,迭代器遍历为中序,默认有序
二、set 深度介绍
1. set 模板参数
cpp
template <class T,
class Compare = less<T>,
class Alloc = allocator<T>>
class set;
- T:存储的 key 类型,也是 value 类型(set 是 key-only 模型)
- Compare :比较规则,默认 less → 升序;可传 greater → 降序
- Alloc:空间配置器,一般使用默认即可
2. set 核心特性
- 底层:红黑树,增删查 O(logN)
- 遍历:中序遍历 → 默认升序
- key 不可修改:迭代器为 const,修改会破坏树结构
- key 唯一:自动去重,重复 key 插入失败
- 支持双向迭代器,支持范围 for
- 要求 T 支持**< 比较**,自定义类型需重载 < 或提供仿函数
三、set 构造与迭代器
1. 常用构造方式
- 无参构造:
set<int> s; - 迭代器区间构造:
set<int> s(v.begin(), v.end()); - 拷贝构造:
set<int> s2(s1); - 初始化列表构造:
set<int> s = {1,2,3,4};
2. 迭代器
- 正向:
begin() / end() - 反向:
rbegin() / rend() - 重要 :iterator 本质是
const_iterator,*不能修改 it
示例:
cpp
set<int> s = {5,2,7,5};
auto it = s.begin();
// *it = 1; 编译报错,key不可修改
while (it != s.end()) {
cout << *it << " ";
++it;
}
// 输出:2 5 7(去重+升序)
四、set 增删查核心接口
1. 插入 insert
cpp
pair<iterator, bool> insert(const value_type& val);
void insert(initializer_list<value_type> il);
- key 不存在:插入成功,返回
(迭代器, true) - key 已存在:插入失败,返回
(迭代器, false) - 批量插入:重复值自动忽略
示例:
cpp
s.insert({2,8,3,9});
2. 查找 find / count
cpp
iterator find(const value_type& val) const;
size_type count(const value_type& val) const;
find:找到返回迭代器,否则返回end()count:存在返回 1,不存在返回 0(快速判存)- 注意 :优先用
set::find,O(logN);算法库find是 O(N)
3. 删除 erase
cpp
iterator erase(const_iterator position);
size_type erase(const value_type& val);
iterator erase(const_iterator first, const_iterator last);
- 按迭代器删:高效,O(logN)
- 按值删:返回删除个数(0 或 1)
- 按区间删:
[first, last)
4. 边界查找(非常实用)
cpp
iterator lower_bound(const value_type& val) const;
iterator upper_bound(const value_type& val) const;
lower_bound:返回 ≥ val 的第一个迭代器upper_bound:返回 > val 的第一个迭代器- 组合使用可精准圈定区间
[val1, val2]
示例:
cpp
// 删除 [30, 60] 区间
auto itlow = s.lower_bound(30);
auto itup = s.upper_bound(60);
s.erase(itlow, itup);
五、multiset 与 set 完整对比
multiset 头文件同样是 <set>,用法几乎一致,核心区别:
- 允许 key 重复:只排序,不去重
insert:一定成功find:返回中序第一个匹配元素count:返回真实重复次数erase(val):删除所有等于 val 的元素
示例:
cpp
multiset<int> s = {4,2,7,2,4,8,4};
// 输出:2 2 4 4 4 7 8
六、set 经典力扣实战
1. LeetCode 349. 两个数组的交集
思路:
- 用 set 对两个数组分别去重+排序
- 双指针遍历,小的指针++,相等收集结果
标准代码:
cpp
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
set<int> s1(nums1.begin(), nums1.end());
set<int> s2(nums2.begin(), nums2.end());
vector<int> ret;
auto it1 = s1.begin(), it2 = s2.begin();
while (it1 != s1.end() && it2 != s2.end()) {
if (*it1 < *it2) ++it1;
else if (*it1 > *it2) ++it2;
else {
ret.push_back(*it1);
++it1, ++it2;
}
}
return ret;
}
};
2. LeetCode 142. 环形链表 II
思路:
- 遍历链表,用 set 存储节点地址
- 插入失败说明已存在,即为入环点
cpp
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
set<ListNode*> s;
ListNode* cur = head;
while (cur) {
auto [it, ok] = s.insert(cur);
if (!ok) return cur;
cur = cur->next;
}
return nullptr;
}
};
总结(set 篇)
- set:key 模型、有序、去重、O(logN)、key 不可改
- multiset:有序、可重复、无去重
- 适用场景:去重、排序、快速查找、判重、区间处理
- 接口与 vector/list 高度统一,极易上手