文章目录
- 📖 前言
- 1. 键值对的引入⚡
- 2. 树形结构的关联式容器🌟
- 3. set的介绍 + 使用⭐
- 4. map的介绍 + 使用⭐
- 🏁4.4.1 利用map统计次数:
- 🏁4.4.2 std::map::operator[]
📖 前言
本章将继续学习STL中的两个很重要的容器map和set,其底层实现是封装了一个红黑树,我们通过本节来学习和深入了解一下这两大容器……🙋 🙋 🙋 🙋 🙋
前情回顾:红黑树模拟实现 👉 传送门
1. 键值对的引入⚡
在之前的红黑树和AVL树的模拟实现中的Kye_value
模型,我们都用到过pair
,下面正式引入:
用来表示具有一 一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代
表键值,value表示与key对应的信息。
例如我们之前提到过的,要建立一个英汉互译的字典,英文单词与其中文含义是一 一对应的关系。
它实际上是一个类模板,STL原码库中给出的源代码如下:
2. 树形结构的关联式容器🌟
序列式容器:
在前期我们所学过的STL容器中,例如:string,vector,list,queue
…,这些序列式容器中我们不难发现,其存储的都是 C++ 基本数据类型(诸如 int、double、float、string
等)或使用自定义类型(结构体)的元素。
关联式容器:
关联式容器则和序列式容器有很大区别,关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的键值对,在数据检索时比序列式容器效率更高。除此之外,序列式容器中存储的元素默认都是未经过排序的,而使用关联式容器存储的元素,默认会根据各元素的键值的大小做升序排序。
3. set的介绍 + 使用⭐
使用 set 容器,必须引入该头文件#include < set >
set使用文档
- T: set中存放元素的类型,实际在底层存储<value, value>的键值对。
- Compare:set中元素默认按照小于来比较
- Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理
set的插入:
与之前学习的容器中用到的接口类似,set也同样提供了插入接口。
void test_set1()
{
set<int> s;
s.insert(4);
s.insert(5);
s.insert(2);
s.insert(1);
s.insert(1);
s.insert(3);
s.insert(2);
s.insert(1);
//如果已经存在则不插入 -- 排序 + 去重
set<int>::iterator it = s.begin();
while (it != s.end())
{
//*it = 10; const 迭代器和普通迭代器都不支持修改
//STL官方库中也是一样的,也是用同一个,只是上层封装名字不一样
cout << *it << " ";
it++;
}
cout << endl;
//底层也是迭代器
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
set<int>::iterator pos = s.find(2);//〇(logN)
if (pos != s.end())
{
cout << "set.find()找到了" << endl;
}
pos = find(s.begin(), s.end(), 2);//〇(N) -- 暴力查找
if (pos != s.end())
{
cout << "找到了" << endl;
}
}
运行结果:
set实现了去重和排序:
- 有重复的不插入
- 默认按照升序排列
同样的迭代器的使用方式也一样,只是这里的const迭代器和普通迭代器都是一样的,都不支持修改,原因是如果对二叉搜索树进行修改的话,很有可能会导致整棵树的结构被打乱,所以不支持修改。
set自带的查找和算法库中的查找有什么区别
- set自带的查找是利用了搜索树的特点,查找时间复杂度为〇(logN)
- 如果用算法库中的查找则是通过暴力查找的方式进行的,时间复杂度为〇(N)
set的删除:
虽然我们在实现红黑树的时候因为红黑树的删除过于复杂并没有实现,但是官方库中也为我们提供了相应的接口。
void test_set2()
{
set<int> s;
s.insert(4);
s.insert(5);
s.insert(2);
s.insert(1);
s.insert(1);
s.insert(3);
s.insert(2);
s.insert(1);
s.erase(3);//直接给值删除
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
int x;
while (cin >> x)
{
set<int>::iterator pos = s.find(x);
if (pos != s.end())
{
s.erase(pos);//迭代器删除
cout << "删除" << x << "成功" << endl;
}
else
{
cout << x << "不在set中" << endl;
}
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
}
还有其他的一些接口,有了之前容器的经验,自学起来很方便。
计算某一个值出现的次数:
对某一区间的值进行操作处理:
***
4. map的介绍 + 使用⭐
使用 map 容器,必须引入该头文件#include < map >
map使用文档
- key: 键值对中key的类型
- T: 键值对中value的类型
- Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比
较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户
自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递) - Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的
空间配置器
map的插入:
map插入的是一对键值对:
void test_map1()
{
map<string, string> dict;
//匿名对象的好处
dict.insert(pair<string, string>("sort", "排序"));
//不用匿名对象
pair<string, string> kv("insert", "插入");
dict.insert(kv);
//make_pair是函数模板
dict.insert(make_pair("left", "左边"));
//可以这么写但是别这么用(隐式类型的转换) -- C++11再讲
dict.insert({ "right", "右边" });
//map的遍历
map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
//cout << *it << endl;//it->operator*() -- C++不支持返回两个值
//cout << (*it).first << ":" << (*it).second << endl;
cout << it->first << ":" << it->second << endl;
it++;
}
cout << endl;
for (const auto& kv : dict)
{
cout << kv.first << ":" << kv.second << endl;
}
}
因为C++不支持返回两个值,所以我们这里用到了pair,通过pair的first和second,即可访问到两个值。
同时,map迭代器的使用方法和其他容器迭代器的使用方法一样,这是STL设计时为了方便使用,所采用的高维度泛型设计。
Key_value模型中,修改不能修改key,但是可以修改value。
🏁4.4.1 利用map统计次数:
如下是利用map来统计各种水果的次数:
通过查找来挨个遍历查找统计个数
void test_map2()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果",
"西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (auto& str : arr)
{
map<string, int>::iterator it = countMap.find(str);
if (it != countMap.end())
{
it->second++;
}
else
{
countMap.insert(make_pair(str, 1));
}
}
}
注意:
- 只要是一对一对的值都可以用pair
- first和second是不允许被修改的
insert的返回值是一个pair,pair的first是一个迭代器(如果插入成功了指向新插入的位置,如果插入失败了,则返回已经存在的结点的位置),pair的second的一个bool值,插入成功是返回true,失败是返回false。
所以我们还可以通过返回值来统计个数
🏁4.4.2 std::map::operator[]
下面提供一种特殊的方式来统计次数:
map::operator[]
- 在之前学习的容器中,operator[]都有随机访问的意思,这里的方括号已经没有了随机访问的意思
- 这里operator[]的参数是key,返回值是value的引用
std::map::operator[]有自己的独特之处
上图红框画出来的部分可以理解为如下:
于是便有了如下的统计次数方式:
因为返回的是value的引用所以,可以修改,这样一来,operator[]兼顾了两个功能:插入 + 修改
同样当map需要插入的时候,也就有了如下的写法:
void test_map5()
{
map<string, string> dict;
//pair构造函数
dict.insert(pair<string, string>("sort", "排序"));
pair<string, string> kv("insert", "插入");
dict.insert(kv);
//make_pair
auto ret1 = dict.insert(make_pair("left", "左边"));
auto ret2 = dict.insert(make_pair("left", "剩余"));
dict["operator"] = "重载"; //插入 + 修改
dict["left"] = "左边、剩余"; //修改
dict["erase"]; //插入
cout << dict["left"] << endl; //查找
//遍历
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
while (it != dict.end())
{
//cout << *it << " ";
//cout << (*it).first << ":" << (*it).second << endl;
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
for (const auto& kv : dict)
{
cout << kv.first << ":" << kv.second << endl;
}
}