全面解析 C++ STL 中的 set 和 map
全面解析 C++ STL 中的 set 和 map
C++ 标准模板库(STL)中的关联式容器以其强大的功能和高效性成为开发者解决复杂数据组织问题的重要工具。其中,set
和 map
是最常用的两类关联容器。本篇博客将从基本特性、底层实现、用法详解、高级案例以及性能优化等多个角度,详细解读它们的设计与使用。
1. 什么是关联式容器
关联式容器是一类根据关键字组织和管理数据的容器。与序列式容器(如 vector
和 list
)相比,关联式容器的主要区别如下:
特性 | 关联式容器(set/map) | 序列式容器(vector/list) |
---|---|---|
数据存储顺序 | 按关键字排序 | 按插入顺序 |
数据访问复杂度 | O(log)O(\log )O(log) | O(1)O(1)O(1) 或 O()O()O() |
是否支持随机访问 | 否 | 是 |
是否支持按索引访问 | 否 | 是 |
关联式容器分为有序和无序两类:
- 有序容器:如
set
和map
,基于平衡二叉树(红黑树)实现,数据按排序规则组织。 - 无序容器:如
unordered_set
和unordered_map
,基于哈希表实现,提供更高效的查速度,但不保证元素顺序。
关联式容器的核心特性
- 键值对:关联式容器通过关键字对元素进行组织,
set
中的关键字即为数据本身,而map
则以键值对形式存储数据。 - 自动排序:有序容器会自动对数据进行排序(升序或自定义规则)。
- 高效操作:插入、删除、查的平均时间复杂度为 O(log)O(\log )O(log)(红黑树实现)。
2. set
容器详解
2.1 基本概念与特性
set
是一种集合数据结构,用于存储唯一且自动排序的元素。它的主要特点如下:
- 数据唯一性:同一元素不能重复插入。
- 自动排序:默认按升序排序,可通过自定义比较器更改排序规则。
- 迭代器类型:
set
支持双向迭代器,不支持随机访问。 - 底层实现:使用红黑树作为存储结构。
2.2 底层实现:红黑树
红黑树的特性
红黑树是一种平衡二叉搜索树,满足以下性质:
- 每个节点是红或黑。
- 根节点是黑。
- 每个叶子节点(
nullptr
或 IL 节点)是黑。 - 如果一个节点是红,则其子节点必须是黑(即红节点不能相邻)。
- 从任意节点到其每个叶子节点的路径都包含相同数量的黑节点。
红黑树的操作
- 插入:通过旋转和重新着,确保平衡性和红黑性质。
- 删除:比插入更复杂,同样通过旋转和着维护树的性质。
- 查:沿树遍历,时间复杂度为 O(log)O(\log )O(log)。
在 set
和 map
中,红黑树用来高效实现元素的有序存储和快速查。
2. 构造函数
set
提供以下几种构造方式:
默认构造:创建一个空集合。
代码语言:javascript代码运行次数:0运行复制set<int> s;
初始化列表构造:直接用 {}
初始化集合。
set<int> s = {, 1, 4, 1, 5, 9}; // 重复元素自动去重
迭代器区间构造:从其他容器的元素构造集合。
代码语言:javascript代码运行次数:0运行复制vector<int> v = {1, 2, , 4};
set<int> s(v.begin(), ());
自定义比较规则:
代码语言:javascript代码运行次数:0运行复制set<int, greater<int>> s = {, 1, 4}; // 按降序排序
2.4 常用操作与复杂度分析
操作 | 函数 | 复杂度 | 说明 |
---|---|---|---|
插入 | insert(value) | O(log)O(\log )O(log) | 插入元素,若已存在则插入失败 |
删除 | erase(value) | O(log)O(\log )O(log) | 删除指定元素 |
查 | find(value) | O(log)O(\log )O(log) | 返回迭代器,指向目标元素 |
统计 | count(value) | O(log)O(\log )O(log) | 判断元素是否存在,结果为 0 或 1 |
遍历 | begin(), end() | O()O()O() | 正向迭代访问所有元素 |
下界/上界 | lower_bound()/upper_bound() | O(log)O(\log )O(log) | 返回 >= / > 某值的第一个元素的迭代器 |
插入操作
代码语言:javascript代码运行次数:0运行复制set<int> s;
auto res = s.insert(10); // 插入 10
if (res.second) {
cout << "插入成功" << endl;
} else {
cout << "插入失败" << endl;
}
查操作
代码语言:javascript代码运行次数:0运行复制if (s.find(20) != ()) {
cout << "到元素 20" << endl;
}
删除操作
代码语言:javascript代码运行次数:0运行复制(10); // 删除值为 10 的元素
遍历
代码语言:javascript代码运行次数:0运行复制for (int x : s) {
cout << x << " "; // 正向遍历
}
for (auto it = s.rbegin(); it != s.rend(); ++it) {
cout << *it << " "; // 反向遍历
}
2.5 特殊操作与技巧
(1) 自定义排序规则
set
默认按升序排序,使用仿函数或 std::greater
可修改排序规则:
set<int, greater<int>> s = {, 1, 4};
(2) 范围删除
删除值在 [low, high)
范围内的所有元素:
(s.lower_bound(10), s.upper_bound(50));
() 应用:求两个数组的交集
代码语言:javascript代码运行次数:0运行复制vector<int> intersection(ct vector<int>& nums1, ct vector<int>& nums2) {
set<int> s1(nums1.begin(), ());
set<int> s2(nums2.begin(), ());
vector<int> result;
for (int x : s1) {
if ((x)) result.push_back(x);
}
return result;
}
2.6 multiset
的区别与应用
multiset
与 set
的区别在于:
multiset
允许存储重复元素。- 插入、删除和查操作的接口与
set
相同,但返回的结果会包含重复项。
multiset<int> ms = {1, 2, 2, };
ms.insert(2); // 再次插入 2
. map
容器详解
.1 基本概念与特性
map
是一个关联式容器,用于存储键值对(key-value
)。与 set
相比,map
不仅存储键(key
),还存储与每个键关联的值(value
)。
map
的主要特点包括:
- 键唯一性:每个键在
map
中都是唯一的。 - 自动排序:默认按照键的升序排序,也可以通过自定义比较器来更改排序规则。
- 底层实现:基于红黑树实现,操作复杂度为 O(log)O(\log )O(log)。
- 支持随机访问:与
set
不同,map
中存储的键值对支持通过键快速查对应的值。
map<int, string> m;
m[1] = "apple"; // 插入键值对 (1, "apple")
m[2] = "banana"; // 插入键值对 (2, "banana")
m[] = "cherry"; // 插入键值对 (, "cherry")
内部存储结构
map
使用红黑树存储数据,保证了所有元素按键值自动排序。在 map
中,每个节点存储一个 pair<ct Key, T>
,其中 ct Key
表示键,T
表示值。红黑树的特点确保了查、插入和删除操作的时间复杂度都为 O(log)O(\log )O(log)。
.2 构造与初始化
map
提供了多种构造方法,以适应不同的使用场景:
默认构造:创建一个空 map
。
map<int, string> m;
初始化列表构造:通过初始化列表直接创建 map
。
map<int, string> m = {{1, "apple"}, {2, "banana"}, {, "cherry"}};
范围构造:从另一个容器(如 set
、vector
等)构造 map
。
vector<pair<int, string>> v = {{1, "apple"}, {2, "banana"}};
map<int, string> m(v.begin(), ());
自定义比较器:通过传入自定义比较器,指定键的排序方式。
代码语言:javascript代码运行次数:0运行复制map<int, string, greater<int>> m; // 降序排序
m[2] = "banana";
m[1] = "apple";
. 常用操作与复杂度分析
操作 | 函数 | 复杂度 | 说明 |
---|---|---|---|
插入 | insert(pair) | O(log)O(\log )O(log) | 插入一个键值对,若已存在则插入失败 |
插入或修改 | operator[] | O(log)O(\log )O(log) | 插入新元素或修改已有元素的值 |
查 | find(key) | O(log)O(\log )O(log) | 查指定键,返回键值对的迭代器 |
统计 | count(key) | O(log)O(\log )O(log) | 查指定键是否存在(map 中为 0 或 1) |
删除 | erase(key) | O(log)O(\log )O(log) | 删除指定键及其对应的值 |
遍历 | begin(), end() | O()O()O() | 正向遍历所有元素 |
下界/上界 | lower_bound(key)/upper_bound(key) | O(log)O(\log )O(log) | 查大于等于某值或大于某值的第一个元素 |
插入与查操作
插入:可以通过 insert
方法插入新的键值对,也可以通过 operator[]
插入或修改键值对。
map<int, string> m;
m.insert({1, "apple"});
m[2] = "banana"; // 插入或修改
查:find
方法返回一个迭代器,指向指定键的键值对,若未到则返回 end()
。
auto it = m.find(1);
if (it != ()) {
cout << "Found: " << it->second << endl; // 输出 "apple"
}
删除操作
删除某个键值对:
代码语言:javascript代码运行次数:0运行复制(1); // 删除键为 1 的元素
.4 遍历与修改
map
提供了多种遍历方法:
范围 for:
代码语言:javascript代码运行次数:0运行复制for (ct auto& [key, value] : m) {
cout << key << ": " << value << endl;
}
传统迭代器:
代码语言:javascript代码运行次数:0运行复制for (auto it = m.begin(); it != (); ++it) {
cout << it->first << ": " << it->second << endl;
}
修改值
可以通过迭代器直接修改值,operator[]
也支持修改已有键的值:
m[2] = "grape"; // 修改键为 2 的值为 "grape"
auto it = m.find(2);
if (it != ()) {
it->second = "orange"; // 通过迭代器修改值
}
.5 特殊操作与进阶技巧
(1) 下界与上界
通过 lower_bound()
和 upper_bound()
方法,可以获取某个键的下界和上界,常用于区间查。
lower_bound(key)
:返回第一个大于等于key
的元素。upper_bound(key)
:返回第一个大于key
的元素。
map<int, string> m = {{1, "apple"}, {2, "banana"}, {, "cherry"}};
auto lb = m.lower_bound(2); // 返回键为 2 或大于 2 的第一个元素
cout << lb->second << endl; // 输出 "banana"
(2) 自定义排序规则
如同 set
,map
也可以通过自定义比较器来实现不同的排序规则。
map<int, string, greater<int>> m = {{1, "apple"}, {, "cherry"}, {2, "banana"}};
for (ct auto& [key, value] : m) {
cout << key << ": " << value << endl;
} // 输出:: cherry 2: banana 1: apple
() 范围删除
删除某个键值范围内的元素,常用于清除一段区间:
代码语言:javascript代码运行次数:0运行复制map<int, string> m = {{1, "apple"}, {2, "banana"}, {, "cherry"}};
(m.lower_bound(2), m.upper_bound()); // 删除键为 2 和 的元素
.6 multimap
的区别与应用
multimap
是 map
的扩展,允许相同的键有多个值(即支持键的冗余)。与 map
的区别在于,multimap
在插入重复键时不会丢失数据,而 map
会自动覆盖原有键。
multimap<int, string> mm;
mm.insert({1, "apple"});
mm.insert({1, "banana"});
mm.insert({2, "cherry"});
for (ct auto& [key, value] : mm) {
cout << key << ": " << value << endl; // 输出:1: apple 1: banana 2: cherry
}
multimap
在某些场景下非常有用,例如存储学生成绩时,可能有多个学生取得相同的分数。
4. 高级案例:综合利用 set
和 map
4.1 查两个数组的交集
代码语言:javascript代码运行次数:0运行复制vector<int> intersection(ct vector<int>& nums1, ct vector<int>& nums2) {
set<int> s1(nums1.begin(), ());
set<int> s2(nums2.begin(), ());
vector<int> result;
for (int x : s1) {
if ((x)) result.push_back(x);
}
return result;
}
4.2 构建词频统计表
代码语言:javascript代码运行次数:0运行复制map<string, int> wordCount(ct vector<string>& words) {
map<string, int> wordMap;
for (ct string& word : words) {
wordMap[word]++;
}
return wordMap;
}
4. 高效查链表中的环
代码语言:javascript代码运行次数:0运行复制bool hasCycle(Listode* head) {
set<Listode*> visited;
while (head != nullptr) {
if (visited.find(head) != ()) {
return true; // 到环
}
visited.insert(head);
head = head->next;
}
return false;
}
5. 性能优化与注意事项
5.1 使用 unordered_map
和 unordered_set
在很多查密集型的应用中,unordered_map
和 unordered_set
基于哈希表实现,提供常数时间复杂度 O(1)O(1)O(1) 的查和插入操作。它们的性能优势适用于不需要保持元素顺序的场景。
5.2 避免不必要的拷贝
当插入大量数据时,可以使用 emplace()
来避免不必要的对象拷贝。emplace()
可以直接构造元素,而无需创建临时对象。
map<int, string> m;
(1, "apple"); // 不会发生拷贝
5. 避免频繁修改键
map
不支持修改键,修改键会导致数据结构破坏。因此,避免频繁修改键,而应使用新的键值对进行插入和删除。
6. 总结
通过本文的详细解析,我们全面了解了 C++ 中 set
和 map
容器的使用、底层实现以及高效操作技巧。掌握这些基本知识后,开发者可以灵活使用 set
和 map
来处理各种复杂的关联数据问题,从而提高程序的效率和可读性。
在实际开发中,选择合适的容器(如 map
与 unordered_map
,set
与 unordered_set
)可以帮助我们应对不同的数据处理需求,避免性能瓶颈。希望通过本文的学习,你能够深入掌握这些强大的容器,提升 C++ 编程技能。
#感谢您对电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格的认可,转载请说明来源于"电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格
下一篇:Linux 环境基础开发工具详解
推荐阅读
留言与评论(共有 19 条评论) |
本站网友 同声翻译 | 11分钟前 发表 |
2 | |
本站网友 套利者 | 20分钟前 发表 |
关联式容器的主要区别如下:特性关联式容器(set/map)序列式容器(vector/list)数据存储顺序按关键字排序按插入顺序数据访问复杂度O(log)O(\log )O(log)O(1)O(1)O(1) 或 O()O()O()是否支持随机访问否是是否支持按索引访问否是关联式容器分为有序和无序两类:有序容器:如 set 和 map | |
本站网友 宿州租房信息 | 22分钟前 发表 |
提供更高效的查速度 | |
本站网友 高温输送带 | 30分钟前 发表 |
operator[] 也支持修改已有键的值:代码语言:javascript代码运行次数:0运行复制m[2] = "grape"; // 修改键为 2 的值为 "grape" auto it = m.find(2); if (it != ()) { it->second = "orange"; // 通过迭代器修改值 }.5 特殊操作与进阶技巧(1) 下界与上界通过 lower_bound() 和 upper_bound() 方法 | |
本站网友 百度通 | 6分钟前 发表 |
若未到则返回 end() | |
本站网友 蜂乃宝 | 16分钟前 发表 |
set 中的关键字即为数据本身 | |
本站网友 张冰玉 | 8分钟前 发表 |
value] | |
本站网友 荔枝湾 | 12分钟前 发表 |
set 中的关键字即为数据本身 | |
本站网友 李海生 | 22分钟前 发表 |
但返回的结果会包含重复项 | |
本站网友 成都购房 | 22分钟前 发表 |
banana 2 | |
本站网友 河南艾滋病 | 17分钟前 发表 |
代码语言:javascript代码运行次数:0运行复制auto it = m.find(1); if (it != ()) { cout << "Found | |
本站网友 豌豆的营养价值 | 7分钟前 发表 |
则其子节点必须是黑(即红节点不能相邻) | |
本站网友 汝阳二手房 | 26分钟前 发表 |
基于平衡二叉树(红黑树)实现 | |
本站网友 王富信 | 10分钟前 发表 |
自动排序:默认按照键的升序排序 | |
本站网友 vlsi | 3分钟前 发表 |
掌握这些基本知识后 | |
本站网友 一对一视频聊天 | 15分钟前 发表 |
" << value << endl; } // 输出: | |
本站网友 广东寰球广业工程有限公司 | 0秒前 发表 |
每个节点存储一个 pair<ct Key | |
本站网友 草一次 | 27分钟前 发表 |
而 map 会自动覆盖原有键 |