C++ STL set 详解
一、set 简介
set 是 C++ STL 提供的有序、不重复关联容器,底层基于红黑树实现。
特点:元素自动排序、容器内无重复值、查找删除效率极高。
#include <set> using namespace std;二、核心特性
- 插入元素自动升序排列
- 不允许存放重复数据
- 底层红黑树,增删查时间复杂度 O(logn)
- 迭代器只读,无法直接修改元素值
三、常用构造方式
cpp// 空集合 set<int> s1; // 数组初始化 int arr[] = {1,3,2,2,5}; set<int> s2(arr, arr+5); // 拷贝构造 set<int> s3(s2);
四、常用 API 用法
1. 插入 insert
s.insert(10); s.insert({2,5,8}); template <class InputIterator> 11 void insert (InputIterator first, InputIterator last);重复元素插入会自动失效。
insert 返回一个 pair:
first:迭代器,指向插入的元素
second:bool,表示是否插入成功(true = 新插入,false = 已存在)
auto res = s.insert(5);
if (res.second)
cout << "插入成功";
else
cout << "元素已存在";2. 遍历容器
// 迭代器遍历 for(set<int>::iterator it = s.begin(); it != s.end(); ++it) { cout << *it << " "; } // C++11 范围for for(auto x : s) cout << x << " ";3. 查找 find
找到返回元素迭代器,没找到返回
end()
auto it = s.find(5); if(it != s.end()) cout << "存在"; // 算法库的查找 O(N) auto pos1 = find(s.begin(), s.end(), x); // set⾃⾝实现的查找 O(logN) auto pos2 = s.find(x);比较算法库的find,效率更高!优先用自身的接口函数。
4. 删除 erase
// 删除⼀个迭代器位置的值 iterator erase (const_iterator position); // 删除val,val不存在返回0,存在返回1 size_type erase (const value_type& val); // 删除⼀段迭代器区间的值 iterator erase (const_iterator first, const_iterator last); s.erase(3); // 按值删除 s.erase(it); // 按迭代器删除 s.clear(); // 清空所有元素5. 大小与判空
s.size(); // 元素个数 s.empty(); // 为空返回true6. 边界查找
s.lower_bound(x); // 第一个>=x的元素 s.upper_bound(x); // 第一个>x的元素7.查找数据
// 查找val,返回Val的个数 size_type count (const value_type& val) const;
五、自定义排序规则
默认从小到大,可重载比较器实现降序
// 降序set set<int,greater<int>> s;自定义结构体排序:重载
<运算符即可。六、常见使用场景
- 数组 / 数据快速去重
- 有序数据存储与维护
- 高频查找、快速判重业务
- 区间最值、边界元素检索
C++ multiset
容器 重复元素 删除数值 count 返回值 适用场景 set 不允许 仅删单个 0 或 1 数据去重、唯一值存储 multiset 允许 删除全部匹配 实际个数 重复数据排序、频次统计
oj题目运用
两个数组的交集
349. 两个数组的交集
https://leetcode.cn/problems/intersection-of-two-arrays/
给定两个数组
nums1和nums2,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序
class Solution { public: vector<int> intersection(vector<int>& nums1, vector<int>& nums2) { set<int> a1(nums1.begin(),nums1.end()); set<int> a2(nums2.begin(),nums2.end()); vector<int> vv; for(auto e:a1) { if(a2.count(e)==1) { vv.push_back(e); } } return vv; } };
环形链表ll
142. 环形链表 II
https://leetcode.cn/problems/linked-list-cycle-ii/
给定一个链表的头节点
head,返回链表开始入环的第一个节点。 如果链表无环,则返回null。如果链表中有某个节点,可以通过连续跟踪
next指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos来表示链表尾连接到链表中的位置(索引从 0 开始)。如果pos是-1,则在该链表中没有环。注意:pos不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。
class Solution { public: ListNode *detectCycle(ListNode *head) { set<ListNode*> ai; ListNode* phead=head; while(phead!=nullptr) { int sizep=ai.size(); ai.insert(phead); if(sizep==ai.size()) return phead; phead=phead->next; } return nullptr; } };
C++ STL map 深度解析
1. 定义与本质
std::map是定义在<map>头文件中的有序关联容器,存储结构为键值对(std::pair<const Key, T>),其中:
cpptemplate < class Key, // map::key_type class T, // map::mapped_type class Compare = less<Key>, // map::key_compare class Alloc = allocator<pair<const Key,T> > //map::allocator_type > class map;
- Key(键):唯一标识元素,不可重复,不可修改(
const修饰);- T(值):与键绑定的数据,可任意修改;
- 容器会自动根据键的升序排序(默认规则)。
2. 核心特性
- 键唯一性:同一个键只能存在一个元素,重复插入会覆盖旧值;
- 自动排序:默认按键的小于(
<)运算符升序排列,支持自定义排序规则;- 双向迭代器:仅支持正向 / 反向遍历,不支持随机访问(不能用
[]下标随机跳转);- 动态大小:无需预先分配内存,自动扩容 / 缩容;
- 底层实现:红黑树(Red-Black Tree)(平衡二叉搜索树)。
3. 适用场景
- 需要有序存储键值对的场景;
- 频繁查找、插入、删除元素,且对稳定性要求高;
- 键需要唯一映射的业务(如用户 ID→用户信息、单词→释义)。
4. pair 介绍
map底层的红⿊树节点中的数据,使⽤ pair<Key, T> 存储键值对数据。
cpptypedef pair<const Key, T> value_type; template <class T1, class T2> struct pair { typedef T1 first_type; typedef T2 second_type; T1 first; T2 second; pair(): first(T1()), second(T2()) {} pair(const T1& a, const T2& b): first(a), second(b) {} template<class U, class V> pair (const pair<U,V>& pr): first(pr.first), second(pr.second) {} }; template <class T1,class T2> inline pair<T1,T2> make_pair (T1 x, T2 y) { return ( pair<T1,T2>(x,y) ); }
map 基础用法
1. 初始化方式
map支持多种初始化方式,覆盖开发中所有常用场景:
cpp// 1. 空初始化 map<int, string> map1; // 2. 列表初始化(C++11 及以上) map<int, string> map2 = {{1, "张三"}, {2, "李四"}, {3, "王五"}}; // 3. 拷贝初始化 map<int, string> map3(map2); // 4. 范围初始化(从其他容器拷贝) map<int, string> map4(map2.begin(), map2.end());2. 插入元素
cpp// 方式1:[] 运算符(最简单,若键不存在则创建,存在则覆盖值) map1[1] = "苹果"; map1[2] = "香蕉"; map1[1] = "红苹果"; // 键1已存在,覆盖旧值 // 方式2:insert() 插入 pair(不覆盖,键存在则插入失败) map1.insert(pair<int, string>(3, "橙子")); map1.insert(make_pair(4, "葡萄")); // 更简洁的 pair 创建方式 // 方式3:insert() 插入初始化列表(C++11) map1.insert({5, "芒果"}); map1.insert({5, "xxxx"});// 插入失败,插入只看key // 方式4:emplace() 直接构造(效率更高,避免临时对象) map1.emplace(6, "西瓜");关键区别:
[]:会覆盖旧值,且能直接访问值;insert()/emplace():不覆盖旧值,键存在时插入操作无效。3. 访问元素
两种安全 / 便捷的访问方式,推荐优先使用
at():
cppmap<int, string> fruitMap = {{1, "苹果"}, {2, "香蕉"}, {3, "橙子"}}; // 方式1:[] 运算符(键不存在会自动创建空值,有风险) cout << fruitMap[1] << endl; // 输出:苹果 // cout << fruitMap[10] << endl; // 危险:键10不存在,自动插入键10,值为空字符串 // 方式2:at() 成员函数(键不存在抛出 out_of_range 异常,更安全) cout << fruitMap.at(2) << endl; // 输出:香蕉 // 方式3:通过迭代器访问 auto it = fruitMap.find(3); if (it != fruitMap.end()) { cout << it->first << ": " << it->second << endl; // 输出:3: 橙子 }4. 查找元素
find()是map最核心的查找方法,时间复杂度 O(logn):
cpp// 查找键2 auto it = fruitMap.find(2); if (it != fruitMap.end()) { cout << "找到元素:" << it->second << endl; } else { cout << "未找到元素" << endl; }补充**:
count(key)函数,返回键的数量(map中只能是 0 或 1),常用于判断键是否存在:**5. 删除元素
cppmap<int, string> fruitMap = {{1, "苹果"}, {2, "香蕉"}, {3, "橙子"}, {4, "葡萄"}}; // 方式1:按迭代器删除 auto it = fruitMap.find(2); if (it != fruitMap.end()) { fruitMap.erase(it); // 删除键2 } // 方式2:按键删除(直接传键) fruitMap.erase(3); // 删除键3 // 方式3:范围删除 fruitMap.erase(fruitMap.begin(), fruitMap.find(4)); // 删除键1,2, 3 // 清空整个map fruitMap.clear();6. 常用成员函数
函数 作用 size()返回元素个数 empty()判断是否为空(空返回 true) max_size()返回容器最大可存储元素数 swap(map)交换两个 map 的内容 lower_bound(key)返回第一个 ≥ key 的迭代器 upper_bound(key)返回第一个 > key 的迭代器
operator[]底层实现原理🔍****核心代码拆解
mapped_type& operator[] (const key_type& k) { return (*(this->insert(make_pair(k, mapped_type()))).first).second; }它的执行过程,正好对应图里从内到外的层层嵌套:
make_pair(k, mapped_type())
- 先构造一个键值对:
pair<key_type, mapped_type>- 这里的
mapped_type()是关键:如果是int就是0,如果是自定义类型就是默认构造函数初始化的对象。
insert(...)
- 调用
map的insert方法,尝试把这个键值对插入容器。- 它的返回值是
pair<iterator, bool>:
bool表示是否插入成功(true表示插入了新元素,false表示键已存在)iterator指向容器中该键对应的元素(无论新插入还是已存在)
insert(...).first
- 取出返回的
pair里的第一个成员,也就是指向元素的iterator。
*(...).first
- 对迭代器解引用,得到
map里的元素,类型是pair<key_type, mapped_type>。
(...).second
- 取出这个
pair里的第二个成员,也就是mapped_type类型的值,然后返回它的引用💡****示例代码的本质
for (auto& str : arr) { countMap[str]++; }
- 当
str第一次出现时:operator[]会自动插入(str, 0),然后返回0的引用,接着执行++,变成1。- 当
str再次出现时:insert会发现键已存在,直接返回指向已有元素的迭代器,然后对值执行++。⚠️****两个关键注意点
**
operator[]会强制插入元素即使你只是想读取countMap["不存在的键"],它也会默认插入一个值为0(或默认构造值)的键值对,这会改变容器的大小。**👉 安全读取应该用find():
auto it = countMap.find(str); if (it != countMap.end()) { // 存在,用 it->second } else { // 不存在,不插入 }
- 对
const map不能用operator[]因为operator[]有修改容器的副作用,所以const std::map不支持[]运算符,只能用find()。
C++ multimap
multimap和map的使⽤基本完全类似,主要区别点在于multimap⽀持关键值key冗余,那么 insert/find/count/erase都围绕着⽀持关键值key冗余有所差异,这⾥跟set和multiset完全⼀样,⽐如find时,有多个key,返回中序第⼀个。
其次就是multimap 不⽀持[] ,因为⽀持key冗余,[]就只能⽀持插⼊了,不能⽀持修改。
OJ题目运用
随机链表的复制
138. 随机链表的复制
https://leetcode.cn/problems/copy-list-with-random-pointer/
给你一个长度为
n的链表,每个节点包含一个额外增加的随机指针random,该指针可以指向链表中的任何节点或空节点。构造这个链表的 深拷贝。 深拷贝应该正好由
n个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的next指针和random指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。例如,如果原链表中有
X和Y两个节点,其中X.random --> Y。那么在复制链表中对应的两个节点x和y,同样有x.random --> y。返回复制链表的头节点。
cppclass Solution { public: Node* copyRandomList(Node* head) { map<Node*,Node*> copymap; Node* copyhead=nullptr,*copytail=nullptr; Node* cur=head; while(cur) { if(copyhead==nullptr) { copyhead=copytail=new Node(cur->val); } else { copytail->next=new Node(cur->val); copytail=copytail->next; } copymap[cur]=copytail; cur=cur->next; } cur=head; Node* copy=copyhead; while(copy) { if(cur->random==nullptr) { copy->random=nullptr; } else { copy->random=copymap[cur->random]; } copy=copy->next; cur=cur->next; } return copyhead; } };
前K个高频单词
692. 前K个高频单词
https://leetcode.cn/problems/top-k-frequent-words/
给定一个单词列表
words和一个整数k,返回前k个出现次数最多的单词。返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。
方法一:排序
cppclass Solution { public: struct compare { bool operator()(const pair<string,int>& x,const pair<string,int>& y) { if (x.second != y.second) { return x.second > y.second; // 频率高的在前 } return x.first < y.first; } }; vector<string> topKFrequent(vector<string>& words, int k) { map<string,int> kv; for(auto& e:words) { kv[e]++; } vector<pair<string,int>> topk(kv.begin(), kv.end()); sort(topk.begin(),topk.end(),compare()); vector<string> topkk; for(int i=0;i<k;i++) { topkk.push_back(topk[i].first); } return topkk; } };补充**:这里因为sort库函数的稳定性差,所以用到了仿函数控制逻辑。**
如果我们使用stable_sort就不会打乱map的默认排序,仿函数就不用设计比较字符串大小
方法二:优先队列
cppclass Solution { public: struct kv_pair{ // 次数大的在前面,次数相等的,字典序小的在前面 bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) { return kv1.second < kv2.second || (kv1.second == kv2.second && kv1.first > kv2.first); } }; vector<string> topKFrequent(vector<string>& words, int k) { map<string,int> countMap; for(auto& str : words) { countMap[str]++; } // 大堆 priority_queue<pair<string,int>,vector<pair<string,int>>, kv_pair> pq(countMap.begin(),countMap.end()); vector<string> ret; for(size_t i = 0;i < k;++i) { ret.push_back(pq.top().first); pq.pop(); } return ret; }


