目录
Explosion !
(最新更新时间——2025.2.17)
🌟1. 关联式容器
我们之前学习过的序列式容器底层为线性序列的数据结构,例如 list ,其节点是线性存储的,一个节点存储一个数据,但这些数据未必有序。而关联式容器则比较特殊,存储的是 <key, value> 的键值对, 这意味着可以按照键值大小 key 以某种规则放置在适当的位置,因此,关联式容器没有首位的概念,因此没有头插尾插等相关操作。
那什么是键值对呢?我们接着来看
🌟2. 键值对(pair,make_pair)
键值对是用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代 表键值,value表示与key对应的信息。
比如:现在要建立一个英汉互译的字典,那该字典中必然 有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应 该单词,在词典中就可以找到与其对应的中文含义。
在标准库中,专门定义了键值对 pair :
我们看一看它的源代码->:
//SGI 版 STL 中的实现 template <class T1, class T2> struct pair { typedef T1 first_type; typedef T2 second_type; T1 first; T2 second; pair() : first(T1()), second(T2()) {} pair(const T1& a, const T2& b) : first(a), second(b) {} };
可见,它有两个模板参数,这两个模板参数又被重命名为first_type与second_type,并且创建了两个对象,first和second
其中, pair 中的 first 表示键值, second 表示实值,在给 关联式容器中插入数据时,就可以构建键值对 pair 对象。
这就意味着我们可以使用first与second这两个对象,对其两个模板参数 T1 , T2 进行访问,
也就是说,first指的是模板第一个参数所对应的值,second指的是模板第二个参数所对应的值
那么到底是什么意思,我们还是看一下代码 -> :
例如,下面构建了一个键值 key 为 string,实值 value 为 int 的匿名键值对 pair 对象
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<string> using namespace std; int main() { pair<string, int> p1 ("hello", 123); cout << p1.first << " " << p1.second; }
此外,库中还设计了一个函数模板 make_pair, 可以根据传入的参数,去调用 pair 构建对象并返回 pair 对象
我们直接看代码->:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<string> using namespace std; int main() { //这样的写法会导致编译报错 //make_pair p2 ("hello", 123); auto p2 = make_pair("hello world", 123); cout << p2.first << " " << p2.second; }
🌟3. set
1.set 实则是二叉搜索树中的 key 模型。key 模型主要的应用场景是判断在不在,例如:门禁系统,车库系统,检查文章中单词拼写是否正确等
2.它的底层就是二叉搜索树(红黑树)
3.set 只包含实值 value,或者说, set中->:实值就是键值,键值就是实值
3.1 set文档介绍
模板参数:
🌟T:底层存储的数据类型
🌟Compare: 比较方式,默认按照小于方式比较
🌟Alloc: set中元素空间的管理方式,使用STL提供的空间配置器管理
set文档介绍:
🌟1.set是按照一定次序存储元素的容器
🌟2.在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。🌟3.set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
🌟4.set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。
🌟5.set在底层是用二叉搜索树(红黑树)实现的
set要点:
⭐️⭐️ set底层是一颗二叉树,内容可以删除,但不允许修改,查找某个元素时间复杂度为logN。
⭐️⭐️ set中不允许出现重复元素,本质是排序+去重
⭐️⭐️ set的正向迭代器遍历是一个升序序列,反向迭代器是一个降序序列。
⭐️⭐️ set存的是一个值value,但是在底层也是一个键值对,不过这个键值对两个值都相同,即<value,value>,我们在使用时,只需要插入就可以,不需要构建键值对。
⭐️⭐️set比较默认按照小于比较
⭐️⭐️对于unique算法,去重相邻重复元素,去重之前需要先排序,才可以达到去重的效果。并不会改变容器的大小,随后调用erase删除这些重复元素
3.2 set构造函数
在这里我们创建时,需要指定实值的类型。
代码展示一下->:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vector> #include<set> using namespace std; int main() { //C++11中的万物都可{ }初始化 set<int> s1 = { 8,3,10,15,3,6,9 }; //迭代器区间构造 vector<int> v1 = { 6,9,8,4,56,34,78 }; set<int> s2(v1.begin(),v1.end()); set<int> s3(s2);//拷贝构造set for (const auto& e : s2) { cout << e << " "; } }
我们上述的构造函数默认是升序的,如果想要更改为逆序,我们需要这样写->:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vector> #include<string> #include<set> using namespace std; int main() { //迭代器区间构造 vector<int> v1 = { 6,9,8,4,56,34,78 }; set<int,greater<int>> s1(v1.begin(),v1.end()); for (const auto& e : s1) { cout << e << " "; } }
3.3 set的常用函数
set的常用函数如下:
接下来我们会逐一代码展示功能:
其实这些函数的使用与我们的vector等的用法相同:
唯一不同的点就是count的使用
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vector> #include<string> #include<set> using namespace std; int main() { //迭代器区间构造 vector<int> v1 = { 6,9,8,4,56,34,78 }; set<int> s1(v1.begin(), v1.end()); //为空返回1,不为空返回0 if (s1.empty()) { cout << "s1为空" << endl;//这里不会打印 } set<string> s2; if (s2.empty()) { cout << "s2为空" << endl;//这里会打印 } s2 = { "hello","world" }; cout << s1.size() << " ";//打印7 cout << s2.size() << " ";//打印2 cout << endl; cout << s1.max_size() << endl;//打印576460752303423487 s2.insert("null");//这里只是插入的意思,因为set的底层是二叉搜索树,所以只是插入 s2.insert(s2.begin(), "oppo");//迭代器位置插入 set<string> s9;//迭代器区间插入 s9.insert(s2.begin(), s2.end()); //字符串的排序是根据ascII值 for (const auto& e : s2) { cout << e << " ";//打印hello null oppo world } cout << endl; for (const auto& e : s9) { cout << e << " ";//打印hello null oppo world } cout << endl; s2.erase("null"); s2.erase(s2.begin());//也支持迭代器删除 for (const auto& e : s2) { cout << e << " ";//打印hello null world } set<int> s3; s1.swap(s3); for (const auto& e : s1) { cout << e << " ";//什么都不打印 //因为s1与s3交换了,所以s1没东西了 } s2.clear();//清空s2的所有内容 auto k = s3.find(4); cout << endl << typeid(k).name() << endl;//打印迭代器类型 cout << endl << s3.count(4);//统计元素4出现了几次 //这里打印1 }
结果如下->:
这里多了解一下:
count 在 set 中只能用作查找,因为 set 键值就是实值,因为其不允许冗余,因此只有一个键值
这样我们用count实现一个查找功能:
//如果存在4,那么s3.count(4)会返回1,也就会打印4存在 if (s3.count(4)) { cout << "4存在" << endl; }
我们上述提到过,set不支持修改key值,因此我们这样写是会报错的->:
因为如果改变了,会破坏二叉搜索树的原则,set 中的普通迭代器在本质上也是 const 迭代器
3.4 lower_bound函数
lower_bound函数会记录大于等于val的迭代器
如果这个值存在,返回这个值的迭代器。
如果这个值不存在,返回大于这个值的迭代器。#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vector> #include<string> #include<set> using namespace std; int main() { //迭代器区间构造 vector<int> v1 = { 6,9,8,4,56,34,78 }; set<int> s1(v1.begin(), v1.end()); set<int>::iterator l1 = s1.lower_bound(6); cout << *l1 << endl;//打印6 set<int>::iterator l2 = s1.lower_bound(35); cout << *l2;//打印56 }
3.5 upper_bound函数
upper_bound也是同样的道理,但是不同的是,这个返回大于val的迭代器
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vector> #include<string> #include<set> using namespace std; int main() { //迭代器区间构造 vector<int> v1 = { 6,9,8,4,56,34,78 }; set<int> s1(v1.begin(), v1.end()); set<int>::iterator l1 = s1.upper_bound(6); cout << *l1 << endl;//不打印6了,打印8 set<int>::iterator l2 = s1.upper_bound(35); cout << *l2;//打印56 }
我们如果想要查找或者删除左闭区间,右闭区间就可以使用这个
3.6 set的特点
🌟4.multiset
multiset的99%都与set相同,唯一不同的是multiset允许重复的值存在,本质就是排序。其他的都与set相同。
🌟5.map
map就是对应我们二叉树中的KV模型
🌟5.1map文档介绍
包括四个值:
🌟Key:键值对中的key值
🌟T:键值对中的value值
🌟Compare:比较器类型,一般是按照小于比较。如果是自定义类型,需要自己传递这个比较方式,达到要求。
🌟Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的
map文档介绍->:
🌟1.map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
🌟2.在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:
typedef pair<const key, T> value_type;🌟3.在内部,map中的元素总是按照键值key进行比较排序的。
🌟4.map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
🌟5.map支持下标访问符,即在[ ]中放入key,就可以找到与key对应的value。
🌟6.map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树)
看一下要点:
🌟🌟map中存储的是一个键值对,键值对中第一个元素,也就是key,根据key值进行排序,确定唯一元素。
第二个值value,与key的内容关联。
🌟🌟元素不可以修改key的值,但是可以修改value的值。
🌟🌟支持下标访问,把key放在[ ]中,可以找到与之对应的value元素
🌟5.2 map的构造函数
map有以下构造方法
这里我们以代码为例->:
1.map是key和value都存在的,所以构造初始化时,需要两个模板参数
2.map的初始化需要依靠pair对象初始化
因此,它的构造函数如下构造:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vector> #include<string> #include<map> using namespace std; int main() { //C++11中的万物都可{ }初始化 std::map<std::string, int> myMap = {{"apple", 5}}; //第一种方法,但是巨麻烦 map<string, int> m1 = { pair<string,int>{"apple",1} }; //也可以像下面这样写 pair<string, int> a1 = { "apple", 1 }; map<string, int> m1; m1.insert(a1); //第二种方法,运用make_pair,方便很多 map<string, int> m2 = { make_pair("pear",2) }; //第三种方法 vector<pair<string, int>> v1 = { {"apple",1},{"pear",2} }; map<string, int> m3; m3.insert(v1[0]); m3.insert(v1[1]); //注意这里的遍历,后面会讲 for (const auto& e : m1) { cout << e.first << " " << e.second; } cout << endl; for (const auto& e : m2) { cout << e.first << " " << e.second; } cout << endl; for (const auto& e : m3) { cout << e.first << " " << e.second; } }
先展示运行代码,再讲解auto
接下来讲解auto遍历
我们知道,map是需要依靠pair来初始化的,而每一个pair具有两块内容,第一块为first,第二块为second,如果我们不指名输出first还是second,那么就会编译报错,因为它不知道该输出哪个
🌟5.3 map的常用函数
可见,map的常用函数与set的常用函数99%都一样(一样指的是只传键值即可,因为map存在key值,与value值,而map的函数用法除了insert与operator[ ]),只有一点不一样,那就是operator[ ]
我们下面展示几个不同的
🌟5.4 insert插入
map的insert插入需要插入pair对象,代码如下->:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vector> #include<string> #include<map> using namespace std; int main() { vector<pair<string, int>> v1 = { {"vivo",1900},{"oppo",2000} }; map<string, int> m3; m3.insert(pair<string,int> {"apple",5 }); m3.insert(make_pair("pear",10)); m3.insert(v1.begin(), v1.end()); for (const auto& e : m3) { cout << e.first << " " << e.second; cout << endl; } }
效果图如下->:
因为map的底层是KV树,它也是二叉搜索树,所以会自动排序
5.4.1 insert不能插入相同的值
我们把上面的代码多加几句->:
map无法插入相同的值,map判断插入的值相不相同只会根据key值去比较,与value值无关
🌟5.5 operator[ ]
我们先看这样的一段代码与效果图->:
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vector> #include<string> #include<map> using namespace std; int main() { vector<string> word = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉", "梨" }; map<string, int> table; for (auto& e : word) table[e]++; for (auto e : table) cout << "<" << e.first << ":" << e.second << ">" << endl; return 0; }
这是怎么实现的? ? ?
其实这是因为operator[ ] 兼顾了这几种功能:插入、修改、插入+修改、查找,接下来进行讲解->:
5.5.1 [ ]插入功能
当使用 operator[] 访问一个 map 中不存在的键时,它会自动插入一个新的键值对到 map 中,并且该键对应的值会被初始化为该值类型的默认值
#include <iostream> #include <map> #include <string> int main() { std::map<std::string, int> myMap; // 访问不存在的键 "apple",会插入该键,值初始化为 0 myMap["apple"]; for (const auto& pair : myMap) { std::cout << pair.first << ": " << pair.second << std::endl; } return 0; }
输出结果:
apple: 0
5.5.2 修改功能
如果 map 中已经存在某个键,使用 operator[ ] 可以直接修改该键对应的值
或者说,如果 map 中已经存在某个键,使用 operator[ ]返回的值是该键对应的值
#include <iostream> #include <map> #include <string> int main() { std::map<std::string, int> myMap = {{"apple", 5}}; // 修改 "apple" 对应的值 myMap["apple"] = 10; //如果不写10(什么都不赋值)不会自动增加 for (const auto& pair : myMap) { std::cout << pair.first << ": " << pair.second << std::endl; } return 0; }
输出结果:
apple: 10
5.5.3 插入 + 修改功能
可以通过 operator[] 先插入一个新的键值对,然后立即修改该值。实际上,这就是前面插入和修改功能的结合使用
#include <iostream> #include <map> #include <string> int main() { std::map<std::string, int> myMap; // 插入 "banana" 键,值初始化为 0,然后修改为 20 myMap["banana"] = 20; for (const auto& pair : myMap) { std::cout << pair.first << ": " << pair.second << std::endl; } return 0; }
输出结果:
banana: 20
5.5.4 查找功能
可以通过 operator[] 来尝试访问某个键对应的值,从而实现查找的目的。不过需要注意的是,如果键不存在,它会进行插入操作,这可能不是我们期望的行为。如果只是单纯想查找,建议使用 find 成员函数
#include <iostream> #include <map> #include <string> int main() { std::map<std::string, int> myMap = {{"apple", 5}}; // 查找 "apple" 对应的值 int value = myMap["apple"]; std::cout << "Value of apple: " << value << std::endl; return 0; }
输出结果:
Value of apple: 5
5.5.5 最开始问题讲解
#define _CRT_SECURE_NO_WARNINGS #include<iostream> #include<vector> #include<string> #include<map> using namespace std; int main() { vector<string> word = { "西瓜", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉", "梨" }; map<string, int> table; for (auto& e : word) table[e]++; for (auto e : table) cout << "<" << e.first << ":" << e.second << ">" << endl; return 0; }
我们回到这个代码,我们以苹果为例,第一次出现苹果时,执行table["苹果"],检测到map中没有改键值,就自动插入,插入后的table["苹果"]会返回该键对应的值,也就会返回0,之后碰到++,就变为了1
以此类推,每一次插入苹果,value值都会++,因此输出了最后的5
5.6 map的特点
🌟6. multimap
multimap 允许键值出现重复
由于 multimap 允许出现多个重复的键值,因此无法使用 operator[ ],因为 operator[ ] 无法确认调用者的意图 -> 不知道要返回哪个键对应的实值。
除此此外 multimap 与 map 没有区别。
最后 : multiset / multimap 用的较少,重点掌握 set / map 即可。
🌟7.完结