【高阶数据结构】map,set,multiset,multimap的使用和介绍
目录
- 一、核心思想:为什么需要 `map` 和 `set`?
- 二、底层基石:共同的实现基础
- 三、`set` 详解:纯粹的集合
- 1. 核心概念
- 2. 关键接口与深度解析
- 四、`map` 详解:键值对字典
- 1. 核心概念
- 2. 关键接口与深度解析
- 五、`map` 和 `set` 的对比总结
- 六、进阶话题:`multiset` 和 `multimap`
- 七、核心概念:关联式容器
- 八、总结与最佳实践
好的,我将基于我的理解,为你系统性地详细讲解 C++ STL 中的
map 和
set。我会从设计哲学到底层实现,再到具体使用,进行由浅入深的剖析。
一、核心思想:为什么需要 map 和 set?
在编程中,我们经常遇到两种基本需求:
- 快速判断一个元素“存在与否”。比如,检查一个用户名是否已被注册。(set)
- 建立一种“对应关系”。比如,通过一个学生的学号,快速找到他的成绩。(map)
vector, list 等序列式容器在处理这些问题时,效率是低下的(需要遍历,O(N)复杂度)。map 和 set 就是为了高效解决这类“查找”问题而生的,它们的核心优势在于 O(log N) 的查找效率。
二、底层基石:共同的实现基础
map 和 set(以及它们的多键版本 multimap, multiset) 在 STL 中通常使用同一种底层数据结构实现:红黑树。
-
什么是红黑树?
它是一种自平衡的二叉搜索树。普通的二叉搜索树在插入有序数据时会退化成链表,查找效率降至 O(N)。红黑树通过一套复杂的规则(节点颜色、旋转)在插入和删除时自动调整,保证了树的高度始终大致平衡。 -
为什么是红黑树?
- 效率保证:得益于平衡性,对红黑树的增、删、查、改操作的时间复杂度都稳定在 O(log N)。这是一个在数据量巨大时依然非常优秀的性能。
- 有序性:对二叉搜索树进行中序遍历,会得到一个有序的序列。这正是
map和set迭代器遍历结果有序的原因。
结论:你可以把 map 和 set 想象成一个封装好的、功能强大的、自动排序且查找飞快的“智能树”。
三、set 详解:纯粹的集合
1. 核心概念
set 是一个存储 唯一元素 的容器。你可以把它理解为一个数学上的集合,或者一个自动去重且排序的数组。
- Key 即 Value:在
set中,你存入的值 (value) 本身就是它的键 (key)。 - 唯一性:集合中不允许存在两个相同的元素。
- 不可变性:一旦元素被插入,你不能修改它的值。因为修改值可能会破坏红黑树的结构。要想修改,只能先删除旧值,再插入新值。
2. 关键接口与深度解析
a. 插入:insert
函数原型:
1. 单个元素插入
pair<iterator,bool> insert(const value_type& val); //value_type:就是set元素
2. 范围插入
template <class InputIterator>void insert(InputIterator first, InputIterator last);
#include <set>
std::set<int> mySet;// 方式1:插入单个值,返回一个pair
std::pair<std::set<int>::iterator, bool> ret = mySet.insert(5);
// ret.first 是指向新插入元素(或已存在元素)的迭代器
// ret.second 是一个bool,表示插入是否成功 (true表示成功,false表示元素已存在)if (ret.second) {std::cout << "插入 5 成功" << std::endl;
} else {std::cout << "5 已存在" << std::endl;
}// 方式2:范围插入,使用初始化列表 (C++11)
mySet.insert({1, 3, 7, 2}); // mySet 现在是 {1, 2, 3, 5, 7}
b. 查找:find
auto it = mySet.find(3); // 查找值为3的元素// !!!最重要的检查!!!
if (it != mySet.end()) {// 找到了,it 是指向 3 的迭代器std::cout << "找到了: " << *it << std::endl;
} else {// 没找到,it 等于 mySet.end()std::cout << "未找到" << std::endl;
}
为什么 find 快? 因为它使用的是红黑树的二分查找,是 O(log N),而不是像在 vector 里线性扫描的 O(N)。
c. 遍历:有序的秘密
// 迭代器遍历
for (std::set<int>::iterator it = mySet.begin(); it != mySet.end(); ++it) {std::cout << *it << " "; // 输出:1 2 3 5 7 (升序)
}// 范围for循环 (更简洁)
for (const auto& num : mySet) {std::cout << num << " ";
}
遍历的顺序就是红黑树的中序遍历结果,所以是有序的。
d. 删除:erase
函数原型:
(1) void erase (iterator position);
(2) size_type erase (const value_type& val);
(3) void erase (iterator first, iterator last);
举例:
// 按值删除
size_t num_removed = mySet.erase(2); // 返回删除的元素个数,对于set是0或1
std::cout << "删除了 " << num_removed << " 个元素" << std::endl;// 按迭代器删除
auto it = mySet.find(5);
if (it != mySet.end()) {mySet.erase(it);
}// 删除一个区间 [first, last)
mySet.erase(mySet.begin(), std::next(mySet.begin(), 2)); // 删除前两个元素
四、map 详解:键值对字典
1. 核心概念
map 是一个存储 键值对 (key-value pair) 的容器。它像一个字典,通过一个 key(如单词)去查找对应的 value(如释义)。
- 键值对:每个元素都是一个
std::pair<const Key, T>。first是key,被声明为const,不可修改。second是value,可以修改。
- Key 的唯一性:
key是唯一的。
2. 关键接口与深度解析
a. 插入:insert
函数原型:
1. 单个元素插入
pair<iterator,bool> insert(const value_type& val); //value_type:就是map元素,即pair
2. 范围插入
template <class InputIterator>void insert(InputIterator first, InputIterator last);
举例:
#include <map>
#include <string>
std::map<std::string, int> studentScores;//单个插入
// 方式1:插入pair
studentScores.insert(std::pair<const std::string, int>("Alice", 95));
// 方式2:使用make_pair (推荐,自动推导类型)
studentScores.insert(std::make_pair("Bob", 88));
// 方式3:使用花括号 (C++11)
studentScores.insert({"Charlie", 72});// insert的返回值同样是一个pair<iterator, bool>
auto ret = studentScores.insert({"Alice", 100});
if (!ret.second) {std::cout << "Key 'Alice' 已存在,插入失败。其值为: " << ret.first->second << std::endl;
}//2. 范围插入
map1.insert({ {1, "one"}, {2, "two"} });
map2.insert(map1.begin(), map1.end());
b. 访问与修改:operator[] - 最强大的功能
这是 map 区别于 set 的最重要接口。
// !!!神奇的特性 !!!
studentScores["David"] = 85; // 1. 如果"David"不存在,会自动插入并赋值为85
studentScores["Bob"] = 92; // 2. 如果"Bob"已存在,会将其value修改为92int score = studentScores["Alice"]; // 3. 可以直接用来访问
std::cout << score << std::endl; // 输出 95
operator[] 的工作原理:
- 在 map 中查找指定的
key("David")。 - 如果找到,返回其
value的引用。 - 如果没找到,则自动插入一个键值对
(key, T()),其中T()是value类型的默认值(int()是 0,string()是空字符串),然后返回这个新value的引用。
注意:正因有此“有则修改,无则插入”的特性,在只希望查询而不希望插入时,应使用find,而不是operator[]。
c. 遍历:访问 key 和 value
// 迭代器访问,it->first 是key,it->second 是value
for (auto it = studentScores.begin(); it != studentScores.end(); ++it) {std::cout << it->first << " 的分数是: " << it->second << std::endl;
}// 范围for循环
for (const auto& kv : studentScores) { // kv 是一个pair的引用std::cout << kv.first << " : " << kv.second << std::endl;
}
d. 查找:find
// 根据key查找
auto it = studentScores.find("Bob");
if (it != studentScores.end()) {it->second = 100; // 可以通过迭代器修改valuestd::cout << "找到了,Bob的分数是:" << it->second << std::endl;
}
e. 删除:erase
函数原型:
(1) void erase (iterator position);
(2) size_type erase (const key_type& k); //pair中的key
(3) void erase (iterator first, iterator last);
举例:
// insert some values:
mymap['a'] = 10;
mymap['b'] = 20;
mymap['c'] = 30;
mymap['d'] = 40;
mymap['e'] = 50;
mymap['f'] = 60;auto it = mymap.find('b');
mymap.erase(it); // erasing by iteratormymap.erase('c'); // erasing by keyit = mymap.find('e');
mymap.erase(it, mymap.end()); // erasing by range
五、map 和 set 的对比总结
| 特性 | set | map |
|---|---|---|
| 存储内容 | 唯一的 key | 唯一的 key 及其对应的 value |
| 元素类型 | Key | pair<const Key, T> |
| 核心用途 | 检查存在性、去重 | 建立映射关系、字典 |
| 修改元素 | 不可修改 key | 不可修改 key,可以修改 value |
| 关键接口 | insert, find, erase | insert, find, erase, operator[] |
| 底层实现 | 红黑树 | 红黑树 |
| 时间复杂度 | 增、删、查:O(log N) | 增、删、查:O(log N) |
| 遍历结果 | 按 key 有序 | 按 key 有序 |
六、进阶话题:multiset 和 multimap
multiset/multimap:允许 重复的key。- 接口差异:
insert总是成功,返回迭代器,不返回pair<...,bool>。erase(key)会删除所有匹配的key,并返回删除的个数。find返回第一个匹配元素的迭代器。count(key)变得有用,返回key的出现次数。equal_range(key)返回一个pair<iterator, iterator>,表示所有等于key的元素范围。这是处理多重键最常用的接口。
multimap没有operator[],因为同一个key可能对应多个value,无法确定返回哪一个。
好的,这是一份关于 C++ STL 中 map 和 set 的系统和仔细的讲解。本文将结合你提供的文章内容,进行更结构化、更深度的剖析。
七、核心概念:关联式容器
- 是什么:
map和set是 C++ 标准模板库中的关联式容器。 - 与序列式容器的区别:
- 序列式容器(vector, list, deque, string):存储的是元素本身,元素在容器中的位置取决于插入的时机和地点,与元素的值无关。底层通常是线性数据结构。
- 关联式容器(map, set, multiset, multimap):存储的是
键值对 (key-value pair)或键 (key),元素在容器中的位置取决于键 (key)的值,与插入顺序无关。底层通常是 平衡二叉搜索树(红黑树)。
- 底层实现:它们通常都基于红黑树实现。这是一种自平衡的二叉搜索树,它保证了在最坏情况下,搜索、插入、删除操作的时间复杂度都是 O(log n)。这正是关联式容器效率高的原因。
- 核心特性:由于底层是二叉搜索树,对其进行中序遍历会得到一个关于 key 的有序序列。
八、总结与最佳实践
-
何时使用?
- 需要快速查找、去重、自动排序 -> 考虑
set。 - 需要建立 key-value 映射关系 -> 考虑
map。 - 允许重复键 -> 考虑
multiset/multimap。 - C++11 后,如果不需要排序,但需要极致查找性能 (O(1)),可以考虑
unordered_set和unordered_map(基于哈希表)。
- 需要快速查找、去重、自动排序 -> 考虑
-
关键要点:
- 始终检查
find的返回值是否等于end()。 - 理解
map::operator[]的“无则插入”特性,谨慎使用。 - 迭代器遍历得到的是有序序列。
- 它们的性能优势在大数据量时尤为明显。
- 始终检查
