跳到主要内容C++ STL 中 map 与 set 容器的核心用法与实战 | 极客日志C++算法
C++ STL 中 map 与 set 容器的核心用法与实战
本文深入讲解了 C++ STL 中 map 与 set 容器的核心用法。首先区分了序列式与关联式容器的特性,重点剖析了 set、multiset、map 及 multimap 的底层原理(红黑树)、构造函数、迭代器操作及增删查改接口。针对 map 的 operator[] 隐式插入机制、pair 类型使用等易错点进行了详细说明,并结合 LeetCode 经典例题(如数组交集、环形链表检测、前 K 个高频单词)展示了容器在实际算法题中的应用技巧。内容涵盖从基础语法到实战优化的完整流程,适合希望提升 C++ 容器运用能力的开发者阅读。
1. 序列式容器和关联式容器
1.1 序列式容器
- 核心特征
- 按插入顺序存储:元素的物理存储顺序与插入顺序一致。
- 允许重复元素:可以存储多个相同的值。
- 通过位置访问:通过索引(如数组)或迭代器直接访问元素。
- 常见容器
vector: 动态数组,支持快速随机访问,尾部插入高效。
list: 双向链表,支持快速插入/删除,但随机访问效率低。
deque: 双端队列,支持头尾高效插入/删除。
array: 固定大小数组,编译时确定大小。
forward_list: 单向链表,内存占用更小。
- 适应场景
- 需要保持插入顺序(如日志记录,操作历史)。
- 频繁通过索引随机访问(如 vector 的 [] 操作)。
- 需要高效头尾操作(如 deque)。
1.2 关联式容器
- 核心特征
- 按键存储:元素基于键值对(key-value)键本身存储。
- 自动排序:元素按键的排序规则(默认升序)组织。
- 唯一性:
set和map要求键唯一;multiset和multimap允许重复键。
- 常见容器
set: 存储唯一键的有序集合。
map: 存储键值对的有序映射。
multiset: 允许重复键的有序集合。
multimap: 允许重复键的有序映射。
- 适用场景
- 需要快速查找(如字典,数据库索引)。
- 需自动排序(如排行榜,有序事件调度)。
- 需要唯一性约束(如用户 ID 管理)。
2. set 系列的使用
2.1 set 类的介绍
set底层是用红黑树实现的,增删查的效率是 O(log n),迭代器遍历走的是搜索树的中序遍历,所以遍历后的元素是有序的。声明如下,T 就是 set底层关键字的类型。
template <class T,
class Compare = less<T>,
class Alloc = allocator<T>>
class set;
默认要求 T 支持小于比较,如果不支持,可以手动实现一个仿函数传给第二个模板参数。一般情况下我们都不需要传后面的两个模板参数。
2.2 set 的构造函数和迭代器
构造方面主要关注无参、区间、拷贝和初始化列表这几种。迭代器是双向迭代器,支持正向和反向遍历。
explicit set(const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type());
template<class InputIterator>
set(InputIterator first, InputIterator last, const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type());
set(const set& x);
set(initializer_list<value_type> il, const key_compare& comp = key_compare(), const allocator_type& alloc = allocator_type());
iterator begin(); iterator end();
reverse_iterator rbegin(); reverse_iterator rend();
2.3 set 的增删查实战
这里我们通过代码看看实际怎么用。注意 set里的元素是不可修改的,因为修改会破坏红黑树的平衡结构。
#include <iostream>
#include <set>
using namespace std;
int main() {
set<int> s;
s.insert(2); s.insert(1); s.insert(5); s.insert(6);
auto it = s.begin();
while(it != s.end()) {
cout << *it << " ";
++it;
}
cout << endl;
s.insert({1, 5, 3, 2, 7, 9});
for(auto e : s) {
cout << e << " ";
}
cout << endl;
set<string> strset = {"sort", "add", "insert"};
for(auto &e : strset) {
cout << e << " ";
}
cout << endl;
return 0;
}
2.4 find 和 erase 的使用样例
erase和find是日常最常用的接口。count可以用来判断元素是否存在,返回值为 0 或 1。
int main() {
set<int> s = {2, 7, 4, 3, 1, 9, 5, 0};
s.erase(s.begin());
int x; cin >> x;
int num = s.erase(x);
if(num == 0) {
cout << x << "不存在!" << endl;
}
cin >> x;
auto pos = s.find(x);
if(pos != s.end()) {
s.erase(pos);
} else {
cout << x << "不存在" << endl;
}
cin >> x;
if(s.count(x)) {
cout << x << "在!" << endl;
} else {
cout << x << "不存在!" << endl;
}
return 0;
}
删除一段区间的值可以用 lower_bound 和 upper_bound 配合 erase 完成。
int main() {
set<int> myset;
for(int i = 1; i < 10; i++) {
myset.insert(i * 10);
}
auto itlow = myset.lower_bound(30);
auto itup = myset.upper_bound(60);
myset.erase(itlow, itup);
for(auto e : myset) {
cout << e << " ";
}
return 0;
}
2.5 multiset 和 set 的差异
multiset和set基本完全相似,主要的区别点在于 multiset支持键值冗余。那么 insert/find/count/erase都围绕着支持键值冗余有所差异。
int main() {
multiset<int> s = {4, 6, 4, 3, 6, 7, 8, 9, 2, 5, 3, 7, 8, 9};
auto it = s.begin();
while(it != s.end()) {
cout << *it << " ";
++it;
}
cout << endl;
int x; cin >> x;
auto pos = s.find(x);
while(pos != s.end() && *pos == x) {
cout << *pos << " ";
++pos;
}
cout << endl;
cout << s.count(x) << endl;
s.erase(x);
for(auto e : s) {
cout << e << " ";
}
return 0;
}
2.6 实战案例:两个数组的交集
利用 set 的特性可以快速去重并排序,非常适合处理这类问题。
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();
auto 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.7 实战案例:环形链表 II
遍历链表,如果节点已存在于 set 中,说明找到了环的入口。
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
set<ListNode*> s;
ListNode* cur = head;
while(cur) {
if(s.count(cur)) return cur;
else s.insert(cur);
cur = cur->next;
}
return nullptr;
}
};
3. map 系列的使用
3.1 map 类的介绍
map底层使用红黑树实现的,增删查改的效率是 O(log n)。Key 的值是有序的。声明如下,Key 就是 map 底层关键字的类型,T 是 map 底层 Value 的类型。
template <class Key,
class T,
class Compare = less<Key>,
class Alloc = allocator<pair<const Key, T>>>
class map;
3.2 pair 类型介绍
map底层红黑树节点中的数据,使用 pair<Key,T>存储键值对数据。pair是一个类模板,里面有两个成员变量,一个是 first,一个是 second。
template<class T1, class T2>
struct pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
};
inline pair<T1, T2> make_pair(T1 x, T2 y) {
return pair<T1, T2>(x, y);
}
3.3 map 的构造与遍历
map支持正向迭代器遍历和反向迭代器遍历,遍历默认按 Key 的升序。支持 value数据的修改,但不支持 Key的修改。修改关键字的数据破坏了底层搜索树的结构。
#include <map>
int main() {
map<string, string> dict = {{"left","左边"},{"right","右边"},{"insert","插入"}};
map<string, string>::iterator it = dict.begin();
while(it != dict.end()) {
cout << it->first << ":" << it->second << endl;
++it;
}
for(auto& e : dict) {
cout << e.first << ":" << e.second << endl;
}
return 0;
}
3.4 map 的增删查改
map的增接口,插入的是 pair 的键值对数据。查和删的接口只用关键字 Key。find返回的是 iterator,不仅仅可以找到 Key 在不在,还能找到映射的 Value,同时通过迭代还能修改 Value。
pair<iterator, bool> insert(const value_type& val);
iterator find(const key_type& k);
size_type erase(const key_type& k);
3.5 map 的数据修改
map 第一个支持修改的方式是通过迭代器,迭代器遍历时或者 find 返回 Key 所在的 iterator 修改。map 还有一个非常重要的修改接口 operator[],但是 operator[]不仅仅支持数据的修改,还支持插入数据和查找数据,所以它是一个多功能复合接口。
mapped_type& operator[](const key_type& k) {
pair<iterator, bool> ret = insert({k, mapped_type()});
iterator it = ret.first;
return it->second;
}
注意:operator[]在查找不到时会隐式插入一个默认值的元素,这在某些场景下可能不是预期的行为,此时应优先使用 find。
3.6 实战案例:统计水果出现次数
#include <iostream>
#include <string>
#include <map>
using namespace std;
int main() {
string arr[] = {"苹果","西瓜","苹果","西瓜","苹果","苹果","西瓜","苹果","香蕉","苹果","香蕉"};
map<string, int> countMap;
for(const auto& str : arr) {
auto ret = countMap.find(str);
if(ret == countMap.end()) {
countMap.insert({str, 1});
} else {
ret->second++;
}
}
for(const auto& e : countMap) {
cout << e.first << ":" << e.second << endl;
}
return 0;
}
3.7 实战案例:前 K 个高频单词
利用 map统计频率后,放入 vector排序即可。注意题目要求的特殊排序规则(频率高在前,频率相同字典序小在前)。
class Solution {
public:
struct Compare {
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]++;
}
vector<pair<string, int>>v(countMap.begin(), countMap.end());
sort(v.begin(), v.end(), Compare());
vector<string> strV;
for(int i = 0; i < k; ++i) {
strV.push_back(v[i].first);
}
return strV;
}
};
4. 总结
set和map都是基于红黑树实现的关联式容器,提供了高效的查找能力。set用于去重和有序存储,map用于建立键值映射。在实际开发中,要注意 map的 operator[]会隐式插入元素,而 multiset/multimap允许重复键。理解这些细节,能帮你写出更健壮、高效的 C++ 代码。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online