关联式容器
键值对
树形结构的关联式容器
关联式容器
在初阶阶段,我们已经接触过
STL
中的部分容器,比如:
vector
、
list
、
deque
、
forward_list(C++11)
等,这些容器统称为序列式容器,因为其底层为线性序列的数据结构,里面存储的是元素本身。那什么是关联式容器?它与序列式容器有什么区别?
关联式容器
也是用来存储数据的,与序列式容器不同的是,其
里面存储的是
<key, value>
结构的键值对,在
数据检索时比序列式容器效率更高
。
键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量
key
和
value
,
key
代表键值,
value
表示与
key
对应的信息
。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
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)
{}
};
键值对实质上就是一对放在结构体中的数据,这两个数据一一对应,找到了其中一个也就找到了另一个
树形结构的关联式容器
根据应用场景的不桶,
STL
总共实现了两种不同结构的管理式容器:树型结构与哈希结构。
树型结构的关联式
容器主要有四种:
map
、
set
、
multimap
、
multiset
。这四种容器的共同点是:使用平衡搜索树
(
即红黑树
)作为其底层结果,容器中的元素是一个有序的序列。下面一依次介绍每一个容器。
set
set的介绍
1. set
是按照一定次序存储元素的容器
2.
在
set
中,元素的
value
也标识它
(value
就是
key
,类型为
T)
,并且每个
value
必须是唯一的。
set
中的元素不能在容器中修改(
元素总是
const)
,但是可以从容器中插入或删除它们。
3.
在内部,
set
中的元素总是按照其内部比较对象
(
类型比较
)
所指示的特定严格弱排序准则进行排序。
4. set
容器通过
key
访问单个元素的速度通常比
unordered_set
容器慢,但它们允许根据顺序对子集进行直接迭代。
5. set
在底层是用二叉搜索树
(
红黑树
)
实现的。
注意:
1.
与
map/multimap
不同,
map/multimap
中存储的是真正的键值对
<key, value>
,
set
中只放
value
,但
在底层实际存放的是由
<value, value>
构成的键值对。
2. set
中插入元素时,只需要插入
value
即可,不需要构造键值对。
3. set
中的元素不可以重复
(
因此可以使用
set
进行去重
)
。
4.
使用
set
的迭代器遍历
set
中的元素,可以得到有序序列
5. set
中的元素默认按照小于来比较
6. set
中查找某个元素,时间复杂度为:logN
7. set
中的元素不允许修改
(
为什么
?)
8. set
中的底层使用二叉搜索树
(
红黑树
)
来实现。
set的使用
set的构造
set的迭代器 set的容量 set修改操作
?我们先来看一下set的遍历
?这时候我们就只能用迭代器去进行遍历了,无法使用[ ]方式,注意:1.范围for也本质是迭代器
2.set这个容器中的数据是不允许被修改的,因为其为一个搜索二叉树,不能修改
set的删除
?这里我们补充一下:我们之前学习别的容器时是没有find的,我们当时使用的是算法中的find,而在这里我们采用的是容器中的find,容器中的find与算法之中的find最大的区别就是时间复杂度不同,算法的为N,这里的为logN,因为本质为树,所以我们尽量使用这里的find,更加高效
map
map的介绍
1. map
是关联容器,它按照特定的次序
(
按照
key
来比较
)
存储由键值
key
和值
value
组合而成的元素。
2.
在
map
中,键值
key
通常用于排序和惟一地标识元素,而值
value
中存储与此键值
key
关联的内容。键值key和值
value
的类型可能不同,并且在
map
的内部,
key
与
value
通过成员类型
value_type
绑定在一起,为其取别名称为pair:
typedef pair value_type;
3.
在内部,
map
中的元素总是按照键值
key
进行比较排序的。
4. map
中通过键值访问单个元素的速度通常比
unordered_map
容器慢,但
map
允许根据顺序对元素进行直接迭代(
即对
map
中的元素进行迭代时,可以得到一个有序的序列
)
。
5. map
支持下标访问符,即在
[]
中放入
key
,就可以找到与
key
对应的
value
。
6. map
通常被实现为二叉搜索树
(
更准确的说:平衡二叉搜索树
(
红黑树
))
。
map的构造
map的迭代器map的容量与元素访问
map中元素的修改
?map的遍历
map同样也支持遍历,我们这里通过匿名对象创建结点,不过这里因为遍历的值为两个,所以我们将其整合为结构体放入pair键值对中,从pair再去访问数据,而且map也是根据中序遍历默认顺序排序的,其实map与set是很相像的,区别是map中是两个值
?
?事实上在工程中我们更多的是使用make_pair的方式去构造pair对象的,更加的简洁
?operator[ ]
我们通过阅读上面的说明可以得知,这个[ ]其实是一个迭代器,而且与插入函数insert密切相关,如果先插入一个元素,那么这个迭代器就指向新插入的元素,第二个参数置为true,代表插入成功,而若插入的元素本来就存在了,则这个迭代器就指向存在了的那个元素的迭代器,第二个参数值为false,代表插入失败
我们通过一个计数函数来了解
?我们可以发现,这个计数函数完成了对于字符串的个数统计,采用的是遍历的方式
?我们再来看这段代码,也是计数功能,可以发现相对上面的代码,这段代码我们利用的是插入函数insert,将数组放到auto中不断进行插入,而后将插入函数的返回值放入到一个pair当中,通过判断插入函数返回的第二个值是否为true来判断是否插入成功,若成功则说明是第一次接触到这个值,进行了插入,若失败则是因为map中已经存在了这个值,而后对其second进行++,当auto遍历结束则完成计数
下面正式引入我们的operator[]函数
mapped_type& operator[](count key_type& k)
{
return(*((this->insert(make_pair(k, mapped_type()))).first)).second;
//最内层(this->insert(make_pair(k, mapped_type()))),这其实就是我们的insert函数,其返回值为pair<irerator, bool>,也就是一个iterator
//下一层解引用pair<irerator, bool>,对这个iterator进行解引用,解引用又是一个pair<key_type , mapped_type>
//最后一层整个对象再取一次second,也就是上层的mapped_type
//这里不用find而用insert的原因是用find假设map中没有k,无法返回,用insert
//1.如果k不在map中,则插入有pair<k,mapped_type()>,在返回映射对象的引用
//2.如果k不在map中,则插入失败,返回k所在节点的中的映射对象的引用
}
?我们可以看到它的返回值很复杂,拆接下来可以分为3层,内层的insert函数返回的pair的first解引用成外部iterator,而后去调用second返回
我们再来看看用operator[]来进行计数的方式
string strs[] = { "西瓜", "黄瓜", "葡萄", "草莓", "黄瓜", "西瓜", "草莓", "西瓜", "西瓜", "苦瓜", "西瓜", "东瓜", "西瓜" };
map<string, int>countMap;
for (auto& str : strs)
{
//1.如果水果不在map中,则operator[]会插入pair<str , 0>,返回映射对象(次数)的引用进行了++
//2.如果水果在map中,则operator[]返回水果对应的映射(次数)的引用,对它++
countMap[str]++;
}
直接对其++即可完成统计,原因是底层的insert若不存在则进行插入,若不存在则因为其返回的引用,也就是对应的次数,直接++即可
下面我们对operator[]操作进行展示
//map中的operator[]三层应用
//1.插入,2.查找k对应的映射对象,3.修改k对应的映射对象
//一般使用operator[]去 1.插入+修改。2.修改。一般不会进行查找,yinweikey如果不在就会进行插入
countMap["香蕉"];//插入
countMap["香蕉"] = 1;//修改
cout << countMap["香蕉"] << endl;//查找
countMap["哈密瓜"] = 5;//插入+修改
for (auto& e : countMap)
{
cout << e.first << "_" << e.second << endl;
}
std::map<std::string, std::string>dict;
dict.insert(make_pair("sort", "排序"));
dict["string"];//插入,但是一般不会这样用
dict["string"] = "字符串";//修改
dict["left"] = "左边"//插入+修改
事实上map中的operatop[]是一个极其特殊的操作符,可以完成许多操作
总结:
1. map
中的的元素是键值对
2. map
中的
key
是唯一的,并且不能修改
3.
默认按照小于的方式对
key
进行比较
4. map
中的元素如果用迭代器去遍历,可以得到一个有序的序列
5. map
的底层为平衡搜索树
(
红黑树
)
,查找效率比较高
6.
支持
[]
操作符,
operator[]
中实际进行插入查找。
multiset
1. multiset
是按照特定顺序存储元素的容器,其中元素是可以重复的。
2.
在
multiset
中,元素的
value
也会识别它
(
因为
multiset
中本身存储的就是
<value, value>
组成的键值对,因此value
本身就是
key
,
key
就是
value
,类型为
T). multiset
元素的值不能在容器中进行修改
(
因为元素总是const
的
)
,但可以从容器中插入或删除。
3.
在内部,
multiset
中的元素总是按照其内部比较规则
(
类型比较
)
所指示的特定严格弱排序准则进行排序。
4. multiset
容器通过
key
访问单个元素的速度通常比
unordered_multiset
容器慢,但当使用迭代器遍历时会得到一个有序序列。
5. multiset
底层结构为二叉搜索树
(
红黑树
)
。
其实我们的multiset与set的操作都是一样的,只不过区别是我们的multset是可以接受多个相同元素的,就是少了去重这个功能,仅有排序
1. multiset
中再底层中存储的是
<value, value>
的键值对
2. multiset
的插入接口中只需要插入即可
3.
与
set
的区别是,
multiset
中的元素可以重复,
set
是中
value
是唯一的
4.
使用迭代器对
multiset
中的元素进行遍历,可以得到有序的序列
5. multiset
中的元素不能修改
6.
在
multiset
中找某个元素,时间复杂度为logN
7. multiset
的作用:可以对元素进行排序
multimap
1. Multimaps
是关联式容器,它按照特定的顺序,存储由
key
和
value
映射成的键值对
<key, value>
,其中多个键值对之间的key
是可以重复的。
2.
在
multimap
中,通常按照
key
排序和惟一地标识元素,而映射的
value
存储与
key
关联的内容。
key
和value的类型可能不同,通过
multimap
内部的成员类型
value_type
组合在一起,
value_type
是组合
key和value
的键值对
:typedef pair<const Key, T> value_type;
3.
在内部,
multimap
中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对
key
进行排序的。
4. multimap
通过
key
访问单个元素的速度通常比
unordered_multimap
容器慢,但是使用迭代器直接遍历multimap中的元素可以得到关于
key
有序的序列。
5. multimap
在底层用二叉搜索树
(
红黑树
)
来实现。
multimap
和
map
的唯一不同就是:
map
中的
key
是唯一的,而
multimap
中
key
是可以重复的
。
不过multimap是没有operator[]的,原因是有多个相同的key,不知道返回哪个key的value
multimap
中的接口可以参考
map
,功能都是类似的。
注意:
1. multimap
中的
key
是可以重复的。
2. multimap
中的元素默认将
key
按照小于来比较
3. multimap
中没有重载
operator[]
操作
(
同学们可思考下为什么
?)
。
4.
使用时与
map
包含的头文件相同:
|