【C++】set 和 map
目录
0. 序列式容器和关联式容器
一、set
1. set 的介绍
2. set 的增删查
3. multiset
二、map
类型别名:
1. map介绍
2. pair
3. map的增删查
4. map的 [ ] 操作
5. multimap
三、map和set的区别
0. 序列式容器和关联式容器
序列式容器
本质:元素按照插入的顺序或指定的位置线性排列。
特点:每个元素都有固定的物理位置(索引),你可以通过位置(下标)直接访问它;两个位置存储的值之间一般没有紧密的关联关系。
代表:
vector(动态数组)、list(双向链表)、deque(双端队列)、array(静态数组)、forward_list(单向链表)。
关联式容器
本质:元素按照Key(键)自动排序,或者通过哈希函数映射。
特点:没有所谓的“物理位置”概念(不能用下标访问),你想找元素,只能通过它的 Key 去查找;它是非线性结构,两个位置有紧密的关联关系,交换一下,它的存储结构就被破坏了。
代表:
map/set(底层红黑树,有序)、unordered_map/unordered_set(底层哈希表,无序)。
一、set
1. set 的介绍
是一个"用平衡二叉搜索树实现的、自动去重、自动排序的集合容器"。
#include <set> std::set<int> s = {5, 1, 3, 3, 2}; // s 里实际是:{1, 2, 3, 5} // 自动排序 ↑ 自动去重 ↑set比较重要的两个特性:
- 只支持增删查;
- 时间复杂度是 O(logn)。
- 不允许修改key
std::set<int>::iterator it = s1.begin(); while (it != s1.end()) { // *it += 1; // 不支持修改key ++it; } cout << endl;2. set 的增删查
set - C++ Reference
(1)insert、erase、find、count...
int main(){ std::set<int> s; s.insert(10); // 插入,重复则忽略 s.insert({ 3, 7, 3 }); // 批量插入,3 只留一个 s.count(10); // 1 存在 / 0 不存在(set 里 count 非 0 即 1) s.find(10); // 返回迭代器,找不到返回 s.end() s.erase(10); // 按值删,返回删除个数 s.erase(s.find(7)); // 按迭代器删 s.empty(); // 是否为空 s.size(); // 元素个数 s.clear(); // 清空 return 0; }(2)lower_bound、upper_bound
- lower_bound(k):下界(闭区间), 返回第一个 >= k 的元素。
- upper_bound(k):上界(开区间),返回第一个 >k 的元素。
std::set<int> s1; s1.insert({ 3,4,2,4,2,1,6,7,8,9,10 }); for (auto e : s1) { cout << e << " "; } cout << endl; // 删除[3,5] // >=3,左闭区间位置 auto it1 = s1.lower_bound(3); // > 5 右开区间位置 auto it2 = s1.upper_bound(5); s1.erase(it1, it2); for (auto e : s1) { cout << e << " "; // 1 2 6 7 8 9 10 } cout << endl;(3)find的时间复杂度
( find返回的是中序的第一个。)
- 算法库里的 find 用的是暴力算法,时间复杂度为 O(N)。
- set 底层是搜索二叉树,find的时间复杂度是O(logN)。
3. multiset
multiset =允许 key 重复的set 。
multiset 和 set 的区别:
| 特性 | set | multiset |
|---|---|---|
| key 重复 | ❌ 不允许 | ✅允许 |
| count(k) | 返回 0 或 1 | 返回 0 ~ n(真实个数) |
| find() | 查找中序的第一个 | |
| erase(k) | 删 0 或 1 个 | 删掉全部k |
| erase(it) | 删 1 个 | 删 1 个 |
| 迭代器可改 | ❌const | ❌const |
| 头文件 | <set> | <set> |
void test_set() { // 相比set不同的是,multiset是排序,但是不去重 multiset<int> s = { 1,2,3,2,4,4,5,6,2,2}; auto it = s.begin(); while (it != s.end()) { cout << *it << " "; ++it; } cout << endl; // 相比set不同的是,x可能会存在多个,find查找中序的第一个 int x; cin >> x; auto pos = s.find(x); while (pos != s.end() && *pos == x) { cout << *pos << " "; ++pos; } cout << endl; // 相比set不同的是,count会返回x的实际个数 cout << s.count(x) << endl; // 相比set不同的是,erase给值时会删除所有的x s.erase(x); for (auto e : s) { cout << e << " "; } cout << endl; }二、map
类型别名:
1. key_type(键的类型)
第一个模板参数。就是在声明 map 时传入的第一个类型,如果是 map<string, int> m;,那么 key_type 就是 string。
2. mapped_type(值的类型)
第二个模板参数。就是在声明 map 时传入的第二个类型,如果是 map<string, int> m;,那么 mapped_type 就是 int。
3. value_type(底层真正存储的元素类型)- 即pair<const key_type, mapped_type>。
1. map介绍
std::map 的功能和 set 类似,会自动排序,主要用于快速查找,去重等;它的底层实现是红黑树。
与 set 不同的是,map 里存储的不是单独的key,而是键值对( key-value ),
key用于排序和标识,value用于存储与key关联的内容。
- map中key不允许重复,value可以重复
- key不允许修改,value可以修改
map<string, string>::iterator it = dict.begin(); while (it != dict.end()) { cout << it->first << ":" << it->second << endl; //it->first = "x"; // 不能修改key it->second = "x"; // 可以修改value ++it; } cout << endl;
map的定义:
2. pair
map底层的红黑树节点中的数据,使用pair存储键值对数据。
template<class K, class V> struct BSTreeNode { BSTreeNode<K, V>* _left; BSTreeNode<K, V>* _right; pair<const K,V>_kv; //map里是这样存的 // ... }; /////// pair /////// typedef pair<const Key, T> value_type; template <class T1, class T2> struct pair { typedef T1 first_type; typedef T2 second_type; T1 first; // key T2 second; // value //... };注意:pair未支持流插入、流提取操作。
3. map的增删查
(1)插入
void test_map1() { map<string, string> dict; dict.insert(pair<string, string>("sort", "排序")); dict.insert(pair<string, string>("insert", "插入")); dict.insert({ "string", "字符串" }); // 隐式类型转换 pair<string, string> kv1 = { "string", "字符串" }; for (auto& kv : dict) { cout << kv.first << ":" << kv.second << endl; } cout << endl; return 0; }insert插入的是一个pair对象,插入的方式有以下几种:
map<string, string> dict = { {"hello","你好"},{"insert","插入"} }; // insert 插入的4种方式 pair<string, string> kv1 = { "left","左边" }; dict.insert(kv1); dict.insert(pair<string, string>("right", "右边")); dict.insert(make_pair("erase", "删除"); dict.insert({ "world","世界" });make_pair 是c++11前的一种写法,它是一个函数模板,返回pair类型的对象,了解即可。
(2)查找
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" }; map<string, int> countMap; for (auto& str : arr) { auto it = countMap.find(str); if (it == countMap.end()) countMap.insert({ str,1 }); else it->second++; } for (auto& kv : countMap) { cout << kv.first << ":" << kv.second << endl; } cout << endl;注意:map类支持 [ ] ,所以上述代码也可以改为:
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" }; map<string, int> countMap; for (auto& str : arr) { countMap[str]++; // 重载了[] } for (auto& kv : countMap) { cout << kv.first << ":" << kv.second << endl; } cout << endl;
(3)删除的方式和set类似,不做赘述。
4. map的 [ ] 操作
若 key 不在 map 中,插入 value_type(key, T()) 并返回其 value 的引用;否则返回已存在元素的 value 引用。
map的[ ] 支持查询,修改,插入的功能。
map<string, string> dict; dict["hello"] = "你好"; // 可以插入 cout << dict["hello"] << endl; // 可以查找 dict["hello"] = "您好"; // 可以修改 cout << dict["hello"] << endl;原理:
std::map::operator[ ]的定义:
mapped_type& operator[] (const key_type& k) { return (*((this->insert(make_pair(k, mapped_type()))).first)).second; }1. 第一层:this->insert(...)
- 调用 map 的 insert 函数。
- (插入key,并调用第二个模板参数(即
T)的默认构造,生成一个默认值mapped_type()。例如 map<string, int>,这就相当于生成了一个 int(0))。 - 重点来了:insert 的返回值是一个pair<iterator, bool>。
- .first是一个迭代器,指向 map 中键为 k 的那个元素(不管这个元素是刚刚新插入的,还是原本就已经存在的)。
- .second 是一个布尔值,表示是否真的插入了(true 表示原来没有,新插了;false 表示原来就有,没插)。
4. 第二层:(...).first
- 我们取出了 insert 返回的 pair 中的 .first,也就是那个迭代器。
- 此时我们手里拿着一个指向 pair<const Key, T> 的迭代器。
- 注:因为我们不关心到底插没插进去(布尔值),只关心能不能拿到那个位置的数据,所以这里直接忽略了 .second。
5. 第三层:*(...)
对迭代器进行解引用(
*),拿到 map 中真正存储的那个元素本身。这个元素的类型是 pair<const Key, T>。
6. 第四层:(...).second 和 return
取出该元素的 .second,也就是我们真正要操作的 value(
T类型)。因为迭代器解引用拿到的是引用,所以这里的 .second 也是引用,return 直接把它作为一个 T& 返回出去。
返回引用就可以支持直接赋值等对value的修改操作。
map<string, string> dict; dict["hello"] = "你好"; // 可以插入 cout << dict["hello"] << endl; // 可以查找 dict["hello"] = "您好"; // 可以修改 cout << dict["hello"] << endl;一句话总结:
它尝试把 (k, T()) 插入 map,如果 k 已经存在则不插入;不管插没插进去,insert 都会返回一个指向该 k 对应元素的迭代器,然后把这个元素的 second(也就是 value)以引用的形式返回。
5. multimap
和map类似,multimap支持值冗余,不支持operator[ ]。
multimap允许重复插入,map的第二次插入无效,value不会被覆盖成新的值。
三、map和set的区别
| 特性 | set | map |
|---|---|---|
| 存储内容 | 只有 Key(单值) | Key + Value(键值对 pair) |
| 取值方式 | 只能获取元素本身 | 可以通过 Key 获取对应的 Value |
| operator[] | ❌ 不支持 | ✅ 支持(m[key] 获取 value) |
| 迭代器取值 | *it (直接就是值) | it->first(Key), it->second(Value) |
| 内存开销 | 较小(只存一个值) | 较大(存两个值) |
| 典型用途 | 去重、排序、判断是否存在、集合运算(交并差) | 字典、计数、键值映射(如 ID 找人) |