前言:主要是自己学习过程的积累笔记,所以跳跃性比较强,建议先自学后拿来作为复习用。
文章目录
1 定义关联容器
1.1 关联容器概述
关联容器 map 和 set 支持高效的关键字查找和访问。
- map (映射)容器的元素是 “关键字–值”,也就是常说的键值对:关键字用于索引,值表示与之关联的数据。
- set (集合)容器的元素只有一个关键字,它支持高效的关键字查询操作,比如查询某个关键字是否在 set 中。set 默认按照关键字的顺序保存元素。
C++ 标准库提供8个关联容器,主要有以下的特点:
- 每个关联容器要么是 map,要么是 set。
- map 和 set 按照关键字的顺序保存元素,且关键字不能重复。
- 容器名字中含有 multi 表明容器允许重复的关键字。
- 容器名字中含有 unordered 表明容器不按关键字的顺序保存元素。
- 无序容器使用哈希函数来组织元素。
关联容器类型 | |
---|---|
有序关联容器 | |
map | 关联数组:保存关键字–值对 |
set | 关键字就是值,set 只保存关键字 |
multimap | 关键字可重复出现的 map |
multiset | 关键字可重复出现的 set |
无序关联容器 | |
unordered_map | 用哈希函数组织的 map |
unordered_set | 用哈希函数组织的 set |
unordered_multimap | 用哈希函数组织的 map,关键字可重复出现 |
unordered_multiset | 用哈希函数组织的 set,关键字可重复出现 |
1.2 pair 类型
介绍关联容器之前,需要先了解 pair 类型,它定义在头文件 utility 中。
pair 是一个生成特定类型的模板。一个 pair 保存两个数据成员,创建它时必须提供数据成员的类型,它们可以不一致:
pair<string, string> pr1; // 保存两个 string
pair<string, size_t> pr2; // 保存一个 string 和一个 size_t
pair<string, vector<int>> pr3; // 保存 string 和 vector<int>
pair 的默认构造函数对数据成员进行值初始化(可参考C++ 学习笔记(三)(标准库类型顺序容器篇)0 初始化的相关知识)。因此 pr1 是包含两个空 string 的 pair,pr2 中的包含一个空 string 和值为0的 size_t 成员,pr3 包含一个空 string 和一个空 vector<int>。
pair 的数据成员是 public 的,两个成员分别被命名为 first 和 second,可以使用成员访问符号访问:
pr1->first; // 访问 pr1 的第一个成员
pr1->second; // 访问 pr1 的第二个成员
pair 上的操作 | |
---|---|
pair<T1, T2> p; | p 是一个 pair,两个成员的类型分别为 T1 和 T2,且都进行了值初始化 |
pair<T1, T2> p(v1, v2) | p 是一个 pair,两个成员的类型分别为 T1 和 T2,且分别用 v1 和 v2 进行了初始化 |
pair<T1, T2> p = {v1, v2}; | 等价于上句 |
pair<T1, T2> p{v1, v2} | 等价于上上句 |
make_pair(v1, v2) | 返回一个用 v1 和 v2 初始化的 pair。成员的类型由编译器根据 v1 和 v2 来推断 |
p.first 或 p->first | 返回 p 的名为 first 的公有数据成员 |
p.second 或 p-> second | 返回 p 的名为 second 的公有数据成员 |
p1 relop p2 | relop 是关系运算符(<、>、<=、>=),按字典序比较大小。 |
p1 == p2 | 当 p1 和 p2 的两个数据成员分别相等时返回 true |
p1 != p2 | 否则返回 false |
如果一个函数返回 pair,那么便可以对返回值进行列表初始化:
pair<stirng, int> process(vector<string> &vec)
{
if (!vec.empty())
return {v.back(), v.back().size()}; // 列表初始化
else return pair<strng, int>(); // 隐式构造函数
}
如果 vec 不为空,则函数返回一个由 vec 中最后一个 string 及其大小组成的 pair,否则返回一个隐式构造的空 pair。上面的花括号也可以改用 make_pair 来实现:
if (!vec.empty())
return make_pair(v.back(), v.back().size());
1.3 定义关联容器
1.3.1 定义 map 和 set
定义 map 时,必须同时指明关键字和值的类型。定义一个 set 时,只需指明关键字的类型即可。我们可以将关联容器初始化为另一个同类型容器的拷贝,或者是从一个范围来初始化关联容器,只要这些值可以转换为所需类型就行。当然,也可以对关联容器进行值初始化:
// mp1 是空容器
map<string, size_t> mp1;
// 列表初始化 st1
set<string> st1 = {"ni", "hao", "ya"};
// 列表初始化 mp2
map<string, string> names = {{"Xiao", "Ming"},
{"Xiao", "Hong"},
{"Xiao", "Gang"}};
1.3.2 定义 multimap 和 multiset
map 和 set 中的每一个元素的关键字都是唯一的。但是 multimap 和 multiset 没有这个限制,它们允许出现多个相同的关键字,比如下面的例子:
// 定义一个含有20个元素的 vector,保存0到9每个整数的两个拷贝
vector<int> vec;
for (vector<int>::size_type i = 0; i != 10; ++i)
{
vec.push_back(i); // 将数 i 存入 vec 中
vec.push_back(i); // 再存一次数 i
}
// st 包含来自 vec 的不重复的元素,m_st 包含 vec 的所有元素
set<int> st(vec.cbegin(), vec.cend());
multiset<int> m_st(vec.cbegin(), vec.cend());
cout << vec.size() << endl;
cout << st.size() <<endl;
cout << m_st.size() <<endl;
示例中用 vec 容器的元素来初始化关联容器,set 定义的关联容器不含有重复元素,所以它的大小是10;而 multiset 定义的关联容器可以含有重复元素,所以它的大小是20。
1.4 有序容器对关键字类型的要求
对于 map 和 set 来说,关键字的类型不非得是内置类型或者 C++ 标准库类型,甚至可以是你自己定义的类。但不管是哪个类型,前提是该类型同时定义了元素比较的方法,因为 map 和 set 的排序原则基于关键字的类型。
类似于向算法提供我们自定义的比较函数(谓词),我们也可以提供自定义的操作来代替关键字上的 < 运算符。但该操作必须是一个严格弱序的操作,也就是“小于等于”。假设说我们已经有了一个自定义的类 test,类中有一个比较函数 compare()。为了使用自定义的比较操作定义集合 set,在定义 set 时必须提供两个类型:关键字类型 test,以及比较操作类型,而该类型应该是一个函数指针类型:
// st 中的元素以 compare 函数的返回结果进行排序
set<test, decltype(compare)*> st(compare);
在这里要注意,使用 decltype 时必须加上一个 “ * ” 来指出我们要使用一个给定函数类型的指针。当我们向 st 中添加元素时,编译器便会调用 compare 函数来为元素排序。
2 关联容器的操作
2.1 关联容器操作概述
关联容器支持所有普通容器的操作,可参考:C++ 学习笔记(三)(标准库类型顺序容器篇)2 容器库概述,但不支持顺序容器的与位置相关的操作,例如 push_front。因为关联容器的根据关键字存储元素,与位置相关的操作对关联容器无效。同时,关联容器也不支持构造函数或插入操作。此外,关联容器的迭代器都是双向的。
下表列出了关联容器有别于顺序容器的额外类型别名:
关联容器的类型别名 | |
---|---|
key_type | 此容器的关键字类型 |
mapped_type | 关键字关联的类型,只有 map 有 |
value_type | 此容器的值类型: 对于 set 来说其就是 key_type;对于 map 来说是 pair<const key_type, mapped_type> |
对于 set 容器,key_type 和 value_type 是一样的,set 中保存的关键字就是值。但是在 map 中,每一个元素都是一个 pair 对象(键值对)。因为元素的关键字无法改变,所以用 const 来定义。我们可以使用作用域运算符来提取一个类型的成员,比如:
set<string>::value_type v1; // v1 是一个 string
set<string>::key_type v2; // v2 是一个 string
map<string, int>::value_type v3; // v3 是一个 pair<string, int>
map<string, int>::key_type v4; // v4 是一个 string
map<string, int>::mapped_type v5; // v5 是一个 int
只有 map 相关的类型才定义了 mapped_type。
2.2 关联容器迭代器
解引用一个关联容器迭代器时得到的是其 value_type 的引用:
map<string, int> mp{ ... }; // 假设对 mp 进行了正确的列表初始化
auto it = mp.begin();
// *it 是一个指向 pair<const string, size_t> 对象的引用
cout << it->first; // 打印元素的关键字
cout << " " << it->second; // 打印元素的值
it->first = "new key"; // 错误:元素的关键字是 const 的,不能修改
++it->second; // 正确:可以通过迭代器改变元素
set 中的关键字也是 const 的,因此尽管 set 容器同时定义了 iterator 和 const_iterator 类型,但这两种类型都只能访问 set 中的元素而不能修改。
在对一个关联容器使用泛型算法时需要注意,由于关键字是 const 的,因此千万不要将关联容器传递给修改或重排容器元素的算法。因为这类算法通常要向元素写入新值,而尝试修改一个 const 往往会出现很多隐藏的错误。如果真的非用不可,关联容器有其特殊的算法供以使用。
2.3 添加元素
关联容器的 insert 成员向容器中添加一个元素或一个元素范围,insert 有两个版本,分别接受一对迭代器,或是一个初始化器列表。而且向容器中插入已经存在的元素没有任何影响:
vector<int> vec = {1, 2, 1, 2}; // vec 有4个元素
set<int> st; // st 初始时是个空集合
st.insert(vec.cbegin(), vec.cend()); // st 只有2个元素
st.insert({3, 4, 3, 4}); // st 现在有4个元素
map 中的元素都是 pair 对象,因此使用 insert 向 map 插入元素时,需要在 insert 的参数列表中创建一个 pair:
map<string, int> mp;
// 向 mp 插入元素的4种方法
mp.insert({"ni", 1});
mp.insert(make_pair("ni", 1));
mp.insert(pair<string, int>("ni", 1));
mp.insert(map<string, int>::value_type("ni", 1));
关联容器 insert 操作 | |
---|---|
c.insert(v) 或 c.emplace(args) | v 是 value_type 类型的对象;args 用来构造一个元素。对于 map 和 set,只有当元素的关键字不在 c 中时才插入(或构造)元素。函数返回一个 pair,包含一个迭代器,它指向具有指定关键字的元素,以及一个指示是否插入成功的 bool 值 |
c.insert(b, e) 或 c.insert(il) | b 和 e 是迭代器,表示一个 c::value_type 类型值的范围;il 是这种值的花括号列表。函数返回 void。对于 map 和 set,insert 函数只插入关键字不在 c 中的元素。对于 multimap 和 multiset,则会插入范围中的每个元素 |
c.insert(p, v) 或 c.emplace(p, args) | 类似 insert(v) 或 c.emplace(args),但迭代器 p 指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素 |
对于接受单一参数的 insert(emplace)函数(上表第一行),其返回值取决于容器类型(因为是重载过的):
- 如果容器不允许包含重复元素,则返回一个 pair 对象,其 first 成员是一个迭代器,指向具有给定关键字的元素;而 second 成员是一个 bool 值,值为 true 表明插入成功,值为 false 表明容器中已有该元素,插入失败。
- 如果容器允许包含重复元素,则返回一个指向新元素的迭代器,而不再返回 bool 值了。因为插入总是成功的。
2.4 删除元素
关联容器定义了三个版本的 erase 函数来删除元素,如下表所示:
从关联容器删除元素 | |
---|---|
c.erase(k) | 从 c 中删除每个关键字为 k 的元素。返回一个 size_type 值,指出删除的元素的数量 |
c.erase§ | 从 c 中删除迭代器 p 指定的元素。p 必须指向 c 中一个真实的元素,而不能等于 c.end()。返回一个指向 p 之后元素的迭代器,若 p 指向 c 中的尾元素,则返回c.end() |
c.erase(b, e) | 删除迭代器 b 和 e 所表示范围中的元素。返回 e |
对于第一个重载 erase 函数,它接受一个 key_type 参数 k,删除所有关键字等于 k 的元素,返回实际删除的元素的数量。对于不包含重复元素的容器,它的返回值总是0或1,0表明容器中没有要删除的元素。
2.5 map 下标操作
map 下标运算符接受一个索引(关键字),并获取与此关键字相关联的值。如果关键字不在 map 中,则会创建一个元素并插入到 map 中,关键字关联的值进行值初始化。
map 和 unordered_map 提供了下标运算符和对应的 at 函数,如下表所示:
map 的下标操作 | |
---|---|
c[k] | 返回关键字为 k 的元素:如果 k 不在 c 中,添加一个关键字为 k 的元素,对其进行值初始化 |
c.at(k) | 先检查参数,若 k 不在 c 中,抛出一个 out_of_range 异常;若在,则访问关键字为 k 的元素 |
有三点要注意:
- set 类型不支持下标运算,因为其没有与关键字相关联的“值”,元素本身就是值,获取一个与关键字相关联的值是没有意义的。
- 同样的,对 multimap 和 unordered_map 也不能进行下标操作,因为这些容器可能有多个值与一个关键字相关联。
- 下标运算符可能会插入一个新元素,所以也不能对用 const 定义的 map 使用下标运算。
通常情况下,对一个容器执行下标运算和解引用该容器迭代器得到的对象是同一个类型的。但map 的有着不同的返回类型。对一个 map 执行下标运算时,会获得一个 mapped_type 对象,也就是关键字关联的值;但解引用一个 map 迭代器时,得到的是一个 value_type 对象,也就是 map 的元素对象。
2.6 访问元素
2.6.1 访问算法
关联容器提供了多种查找指定元素的算法,如下表所示:
查找元素的操作 | |
---|---|
c.find(k) | 返回一个迭代器,指向第一个关键字为 k 的元素,若 k 不在容器中,则返回尾后迭代器 |
c.count(k) | 返回关键字等于 k 的元素的数量。对于不允许重复关键字的容器,返回值永远是0或1 |
c.lower_bound(k) | 返回一个迭代器,指向第一个关键字大于等于 k 的元素 |
c.upper_bound(k) | 返回一个迭代器,指向第一个关键字大于 k 的元素 |
c.equal_range(k) | 返回一个迭代器 pair,表示关键字等于 k 的元素的范围。若 k 不存在,pair 的两个成员均等于 c.end() |
两点需要注意:
- lower_bound 和 upper_bound 不适用于无序容器。
- 下标和 at 操作只适用于非 const 的 map 和 unordered_map。
对于 map 和 unordered_map 容器,下标运算符是最简单的获取元素的方法。但是这样做的弊端在于,如果关键字并不在 map 里面,下标操作就会插入一个新的元素,这可能并不是我们所希望的结果。所以要查找一个元素是否在 map 中,最好还是使用 find 函数。
2.6.2 找出所有关键字相等的元素
有三个方法可以找出一个 multimap 或 multiset 中所有等于给定关键字的元素。假定现在有一个 multimap,它保存着每个作者的所有著作(也因此该 multimap 必须包含重复元素,因为一个作者可能不止一部著作),现在希望找出某个指定作者的所有著作。multimap 的定义如下所示:
// authors 保存了所有作者的所有作品,first 成员是作者的名字,second 成员是其作品名
multimap<string, string> authors = { ... }; // 对 authors 进行初始化
2.6.2.1 方法一
先利用 count 函数找出指定作者的著作数量,再利用 find 函数获得指向作者第一部著作的迭代器,再利用循环逐个打印作者的著作即可:
string name("Lu Xun"); // 要查找的作者名字
auto entries = authors.count("name"); // 该作者的著作数量
auto it = authors.find("name"); // 该作者的第一部著作
// 用一个循环查找此作者的所有著作
while (entries)
{
cout << it->second << endl; // 打印每一部著作的名字
++it; // 迭代器前进到下一部著作
--entries; // 已经打印了一个,著作数量减一
}
即使该作者没有著作也没关系,此时 entries 的值为0,循环不会执行。
2.6.2.2 方法二
利用 lower_bound 函数获得指向作者第一部著作的迭代器,再利用 upper_bound 函数获得指向在该作者之后第一部不是他的著作的迭代器,如下图所示。这两个函数的返回值刚好确定了一个与关键字匹配的元素范围,该范围内的所有元素的关键字都等于给定关键字。因此再利用一个循环即可找出该作者的所有著作:
auto beg = authors.lower_bound; // 获取起始迭代器
auto end = authors.upper_bound; // 获取末尾迭代器
for (; beg != end; ++beg)
cout << beg->second << endl; // 打印每一部著作的名字
如果给定关键字是容器中最大的关键字,lower_bound 返回的指向第一个等于给定关键字的元素,而 upper_bound 返回的则是尾后迭代器,循环依然会执行。
如果给定关键字不存在于容器中,但它比容器中的任何元素都要大,这两者返回的都是尾后迭代器,循环就不会执行。
如果给定关键字不存在与容器中,且它的大小正好位于容器的中部位置,则这两者返回的都是指向第一个大于给定关键字的元素的迭代器,而这个位置便可以用作新元素的插入位置。
2.6.2.3 方法三
最直接的方法便是使用 equal_range 函数,它返回一个 pair 对象,其 first 成员等价于 lower_upper 的返回值,second 成员等价于 upper_bound 的返回值,其两个成员确定了一个与关键字匹配的元素范围,相当于该函数同时实现了方法二中两个函数的效果:
auto pos = authors.equal_range("name")
for (; pos.first != pos.second; ++pos.first)
cout << pos.first->second << endl; // 打印每一部著作的名字
3 无序关联容器
3.1 无序容器的使用
C++ 新标准定义了四个无序关联容器。这些容器不是用比较运算符来组织元素,而是用一个哈希函数和关键字类型的 “==” 运算符。它比较适合对于元素的排序没有太大要求的情境中,因为要维护元素的顺序代价其实比较大。
无序容器提供了与有序容器相同的操作,比如 find、count 等。无序容器也有允许重复关键字的版本。通常情况下,可以用一个无序容器来替代对应的有序容器,只不过输出会不一样罢了。
3.2 管理桶
不同于有序容器对于元素采用顺序存储,它的存储可以理解为一组桶,每个桶保存零个或多个元素。无序容器使用哈希函数来将元素映射到相应的桶中,对于允许重复关键字的无序容器,也有可能多个元素映射到同一个桶中。所以无序容器的性能取决于哈希函数的质量,和桶的数量及大小。
当一个桶保存多个元素时,需要顺序搜索这些元素来查找我们想要的那个,计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是如果一个桶里保存了过多的元素,查找某个特定的元素就需要大量的比较操作。
无序容器提供了一组管理桶的函数,从而允许我们查询容器的状态以及在必要时强制容器进行重组:
无序容器管理操作 | |
---|---|
桶接口 | |
c.bucket_count() | 正在使用的桶的数目 |
c.max_bucket_count() | 容器能容纳的最多的桶的数量 |
c.bucket_size(n) | 第 n 个桶中有多少个元素 |
c.bucket(n) | 关键字为 k 的元素在哪个桶中 |
桶迭代 | |
local_iterator | 可以用来访问桶中元素的迭代器 |
const_local_iterator | 桶迭代器的 const 版本 |
c.begin(n),c.end(n) | 桶 n 的首元素迭代器和尾后迭代器 |
c.cbegin(n),c.cend(n) | 与前两个函数类似,但返回 const_local_iterator |
哈希策略 | |
c.load_factor() | 每个桶的平均元素数量,返回 float 值 |
c.max_load_factor() | c 试图维护的平均桶大小,返回 float 值。c 会在需要时添加新的桶,以使得 load_factor <= max_load_factor |
c.rehash(n) | 重组存储,使得 bucket_count >= n 且 bucket_count > (size / max_load_factor) |
c.reserve(n) | 重组存储,使得 c 可以保存 n 个元素且不必 rehash |
3.3 无序容器对关键字类型的要求
默认情况下,无序容器使用关键字类型的 “==” 运算符来比较元素,它们还使用一个 hash<key_type> 类型的对象来生成每个元素的哈希值。C++ 标准库为内置类型(包括指针)提供了 hash 模板。同时也为一些标准库类型(包括 string)以及智能指针类型定义了 hash。因此无序容器的关键字类型可以是内置类型(包括指针)、标准库类型(包括 string)以及智能指针类型。
但是同1.3 有序容器对关键字类型的要求中所写的一样,如果我们希望无序容器的关键字类型是自定义类,则需要同时提供 “==” 运算符和 hash 值计算函数。这一点将在XX中写到。
希望本篇博客能对你有所帮助,也希望看官能动动小手点个赞哟~~。