跳到主要内容C++算法
C++ map 与 set 容器详解:从原理到实战
C++ STL 中的 set 和 map 容器基于红黑树实现,具备自动排序特性。set 用于存储唯一键,map 用于键值对映射。文章详细解析两者的构造、增删查改接口、迭代器用法及 multiset/multimap 的差异,并通过实际代码示例展示在数组去重、链表判环及单词统计等场景中的应用技巧。重点讲解 operator[] 的隐式插入行为及 multiset 的冗余处理逻辑。
黑客8 浏览 C++ map 与 set 容器详解:从原理到实战
在 C++ STL 中,容器是数据管理的基石。其中序列式容器和关联式容器各有千秋,理解它们的底层机制与使用场景,能显著提升代码效率。
1. 序列式容器和关联式容器
1.1 序列式容器
- 核心特征
- 按插入顺序存储:物理顺序与逻辑顺序一致。
- 允许重复元素:可存储多个相同值。
- 通过位置访问:支持索引或迭代器直接访问。
- 常见容器
vector: 动态数组,随机访问快,尾部插入高效。
list: 双向链表,插入/删除快,但随机访问慢。
deque: 双端队列,头尾操作高效。
array: 固定大小,编译期确定。
forward_list: 单向链表,内存占用更小。
- 适用场景
- 需保持插入顺序(如日志、历史记录)。
- 频繁随机访问(如 vector 的 [] 操作)。
- 需高效头尾操作(如 deque)。
1.2 关联式容器
- 核心特征
- 按键存储:基于键值对组织。
- 自动排序:默认升序排列。
- 唯一性约束:
set/map 要求键唯一;multiset/multimap 允许重复。
- 常见容器
set: 唯一键的有序集合。
map: 键值对的有序映射。
multiset: 允许重复键的有序集合。
multimap: 允许重复键的有序映射。
- 适用场景
- 快速查找(如字典、数据库索引)。
- 需自动排序(如排行榜)。
- 需要唯一性约束(如用户 ID 管理)。
2. set 系列的使用
2.1 set 类的介绍
set 底层由红黑树实现,增删查复杂度为 O(log n)。遍历即中序遍历,结果自然有序。默认要求元素类型支持小于比较,若不支持可传入仿函数。
template <class T,
class Compare = less<T>,
class Alloc = allocator<T>>
class set;
2.2 构造函数与迭代器
构造时通常只需关注无参、区间拷贝及初始化列表三种方式。迭代器为双向迭代器,支持正向与反向遍历。
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(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 中的元素不可修改,因为会破坏红黑树结构。
#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 << " ";
}
return 0;
}
2.4 find 和 erase 的使用
erase 返回被删除元素的个数,find 返回迭代器。利用 count 可以快速判断元素是否存在。
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;
}
auto pos = s.find(x);
if (pos != s.end()) {
s.erase(pos);
}
if (s.count(x)) {
cout << x << "在!" << endl;
} else {
cout << x << "不存在!" << endl;
}
return 0;
}
删除区间值
利用 lower_bound 和 upper_bound 可以高效删除一段范围内的元素。
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);
return 0;
}
2.5 multiset 和 set 的差异
主要区别在于 multiset 允许键值冗余。find 返回第一个匹配项,count 返回总个数,erase(key) 会删除所有匹配项。
int main() {
multiset<int> s = {4, 6, 4, 3, 6, 7, 8, 9, 2, 5, 3, 7, 8, 9};
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);
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;
s.insert(cur);
cur = cur->next;
}
return nullptr;
}
};
3. map 系列的使用
3.1 map 类的介绍
map 存储键值对,底层同样是红黑树。Key 必须唯一且可比较,Value 可修改。遍历顺序按 Key 升序。
template <class Key,
class T,
class Compare = less<Key>,
class Alloc = allocator<pair<const Key, T>>>
class map;
3.2 pair 类型介绍
map 内部节点存储的是 pair<const Key, T>。pair 包含 first 和 second 成员。
template <class T1, class T2>
struct pair {
typedef T1 first_type;
typedef T2 second_type;
T1 first;
T2 second;
};
template<class T1, class T2>
inline pair<T1, T2> make_pair(T1 x, T2 y);
3.3 map 的构造与遍历
支持多种构造方式,包括初始化列表。Key 不可修改,Value 可修改。
map<string, string> dict = {
{"left", "左边"},
{"right", "右边"},
{"insert", "插入"}
};
for (auto& e : dict) {
cout << e.first << ":" << e.second << endl;
}
3.4 map 的增删查改
insert 返回 pair<iterator, bool>,指示是否插入成功。operator[] 功能强大,兼具查找、插入和修改功能,但需注意它会在 Key 不存在时隐式创建默认 Value。
pair<iterator, bool> insert(const value_type& val);
mapped_type& operator[](const key_type& k) {
pair<iterator, bool> ret = insert({k, mapped_type()});
return ret.first->second;
}
3.5 统计单词出现次数
这是 map 最经典的用法之一。推荐使用 operator[] 简化代码,或者用 find 避免不必要的默认构造开销。
#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.6 multimap 和 map 的差异
multimap 支持 Key 冗余,因此不支持 operator[](无法确定修改哪一个 Value),仅支持 insert 等接口。
3.7 实战案例:随机链表的复制
利用 map 建立原节点与拷贝节点的映射关系,解决 random 指针指向问题。
class Solution {
public:
Node* copyRandomList(Node* head) {
map<Node*, Node*> nodeMap;
Node* copyhead = nullptr, *copytail = nullptr;
Node* cur = head;
while (cur) {
if (!copytail) {
copyhead = copytail = new Node(cur->val);
} else {
copytail->next = new Node(cur->val);
copytail = copytail->next;
}
nodeMap[cur] = copytail;
cur = cur->next;
}
cur = head;
Node* copy = copyhead;
while (cur) {
copy->random = cur->random ? nodeMap[cur->random] : nullptr;
cur = cur->next;
copy = copy->next;
}
return copyhead;
}
};
3.8 实战案例:前 K 个高频单词
统计频率后,将 map 转为 vector 进行排序。注意稳定性要求,可使用 stable_sort 或自定义比较器配合 priority_queue。
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());
stable_sort(v.begin(), v.end(), [](const auto& a, const auto& b){
return a.second > b.second || (a.second == b.second && a.first < b.first);
});
vector<string> retv;
for (size_t i = 0; i < k; ++i) {
retv.push_back(v[i].first);
}
return retv;
}
};
掌握这些容器的底层原理与 API 细节,能让你在处理复杂数据结构问题时游刃有余。在实际开发中,根据是否需要排序、是否允许重复来选择正确的容器,是编写高性能 C++ 代码的关键。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,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