关联容器
1 使用关联容器
关联容器支持高效的关键字查找和访问,两个主要的关联容器类型是map
和set
。map
中的元素是关键字-值(key-value)对:关键字起到索引作用,值则表示与索引相关联的数据。set
中每个元素只包含一个关键字,set
支持高效的关键字查询操作——检查一个给定的关键字是否在set
中
标准库提供8个关联容器,如下表格所示。类型map
和multimap
定义在头文件map中;类型set
和multiset
定义在头文件set中;无序容器则定义在头文件unordered_map和unordered_set中。
名称 | 说明 |
---|---|
map | 关联数组;保存关键字-值对 |
set | 关键字即值,即只保存关键字的容器 |
multimap | 关键字可重复出现的map |
multiset | 关键字可重复出现的set |
无序集合
名称 | 说明 |
---|---|
unordered_map | 用哈希函数组织的map |
unordered_set | 用哈希函数组织的set |
unordered_multimap | 哈希组织的map;关键字可以重复出现 |
unordered_multiset | 哈希组织的set;关键字可以重复出现 |
使用map
一个经典的使用关联数组的例子,单词计数程序示例如下:
//统计每个单词在输入中出现的次数
map<string, size_t> word_count; //string到size_t的空map
string word;
while (cin >> word)
++word_count[word]; //提取word的计数器并将其加1
for(const auto &w : word_count) //对map中的每个元素打印结果
{
count << w.first << " occurs " << w.second
<< ((w.second > 1) ? " times" : " time") << endl;
}
其中++word_count[word]
是使用一个string作为下标,获得与此string相关联的size_t类型的计数器。如果word还未在map中,下标运算符会创建一个新元素,其关键字为word,值为0,然后不管元素是否是新创建,都将其值加1;
当从map中提取一个元素是,会得到一个pair
类型的对象,pair是一个模板类型,保存两个名为first和second的(公有)数据成员。其中map使用的pair用first成员保存关键字,用second成员保存对应的值。
使用set
如这样一个例子,使用set保存想忽略的单词,只对不在集合中的单词统计出现次数:
//统计输入中每个单词出现的次数
map<string, size_t> word_count; //string到size_t的空map
set<string> exclude = {"The", "But", "And", "Or", "An", "A",
"the", "but", "and", "or", "an", "a"};
string word;
while (cin >> word)
{
//只统计不在exclude中的单词
if (exclude.find(word) == exclude.end())
{
++word_count[word]; //获取并递增word的计数器
}
}
2 关联容器概述
2.1 关联容器概述
当定义一个map时,必须指明关键字类型和指明值类型。定义一个set时,只需要指明关键字类型。multimap和multiset则是允许多个元素具有相同的关键字。
标准库使用关键字类型的<运算符来比较关键字。也可以自定义操作来代替关键字上的<运算符,示例如下:
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn < rhs.isbn();
}
//使用multiset容器
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
这里使用decltype来指出自定义操作的类型,当使用decltype来获得一个函数指针类型时,必须加上一个*
来指出要使用一个给定函数类型的指针。用compareIsbn来初始化bookstore对象,这表示向bookstore添加元素时,通过调用compareIsbn来为这些元素排序。
2.2 pair类型
pair类型定义在头文件utility中。一个pair保存两个数据成员,两个成员均是public的,其命名为first和second。
操作 | 名称 |
---|---|
pair<T1, T2> p; | p是一个pair,成员类型为T1 和T2 |
pair<T1, T2> p(v1, v2); | p是一个成员类型为T1 和T2 的pair |
pair<T1, T2> p = {v1, v2}; | 等价于p(v1, v2) |
make_pair(v1, v2) | 返回一个用v1 和v2 初始化的pair |
p.first | 返回p名为first的(公有)数据成员 |
p.second | 返回p名为second的(公有)数据成员 |
p1 relop p2 | 关系运算符(<、<、<=、>=)按字典定义 |
p1 == p2 p1 != p2 | 判断两个pair是否相等,当first和second都相等时,两个pair才相等 |
3 关联容器操作
别名 | 描述 |
---|---|
key_type | 此类型的关键字类型 |
mapped_type | 每个关键字关联的类型,只适用于map |
value_type | 对于set,与key_type 相同;对于map,为pair <cosnt key_type, mapped_type> |
tips:注意上述pair类型中的关键字为const
类型
3.1 关联容器迭代器
遍历关联容器
map和set容器都支持begin和end操作,使用迭代器来遍历容器,示例如下:
//获取一个指向首元素的迭代器
auto map_it = word_count.cbegin();
//比较当前迭代器和尾迭代器
while (map_it != word_count.cend)
{
//解引用迭代器,打印关键字-值对
cout << map_it->first << " occurs "
<< map_it->second << " times" << endl;
++map_it; //递增迭代器,移动到下一个元素
}
3.2 添加元素
由于map和set(以及对应的无序类型)包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响。但向map中进行insert操作时,必须记住元素类型是pair。
操作 | 描述 |
---|---|
c.inset(v) | v是value_type类型的对象;args 用来构造一个元素 |
c.emplace(agrs) | 对于map和set,只有当元素关键字不存在c中时才构造元素,函数返回一个pair |
c.insert(b,e) | b和e迭代器,表示一个c::value_type类型值的范围; |
c.insert(il) | il 是这种值的花括号列表 |
c.insert(p, v) | 将迭代器p作为一个提示 |
c.emplace(p, args) | 指出从哪里开始插入新元素应该储存的位置 |
检测insert的返回值
对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,pair的first成员是一个迭代器,指向具有给定关键字的元素;second成员是一个bool值,指出元素是插入成功,还是已存在于容器中,若已存在,则返回false。使用insert的示例如下:
map<string, size_t> word_count; //从string到size_t的空map
string word;
while (cin >> word)
{
//添加一个元素,关键字等于word,值为1
//若word已在word_count中,insert什么也不做
auto ret = word_count.insert({word, 1});
if (!ret.second) //word已在word_count中
++ret.first->second; //递增计数器
}
展开递增语句
++ret.first->second; //递增计数器
ret
保存insert返回的值,是一个pair;
ret.first
是pair的第一个成员,是一个map迭代器,指向具有给定关键字的元素
ret.first->
解引用此迭代器,提取map中的元素,元素也是一个pair
ret.first->second
map中元素的值部分
++ret.first->second
递增此值
3.2 删除元素
操作 | 描述 |
---|---|
c.erase(k) | 从c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量 |
c.erase(p) | 从c中删除迭代器p指定的元素,元素必须存在,返回指向p之后元素的迭代器 |
c.erase(b, e) | 删除迭代器对b和e所表示的范围中的元素,返回e |
3.3 map的下标操作
map下标运算符接受一个索引(即一个关键字),来获取与此关键字相关联的值。如果关键字不在map中,会为它创建一个元素插入到map中,关联值将进行值初始化。因此,只可以对非const的map使用下标操作。
操作 | 描述 |
---|---|
c[k] | 返回关键字为k的元素,若不存在k,则创建k |
c.at[k] | 访问关键字为k的元素,带参数检查。若不存在k,则抛出out_of_range |
对map进行下标操作时,会获得一个mapped_type对象;当解引用一个map迭代器时,会得到一个value_type对象
3.4 访问元素
操作 | 描述 |
---|---|
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的元素的范围 |
Tips:lower_bound和upper_bound不适用与无序容器;下标和at操作只适用于非const的map和unordered_map
4 无序容器
新标准定义了4个无序关联容器,这些容器使用一个哈希函数和关键字类型的==运算符
使用无序容器
使用unordered_map重写最初的单词计数程序
//统计出现次数,但单词不会按字典序排列
unordered_map<string, size_t> word_count;
string word;
while(cin >> word)
++word_count[word]; //提取并递增word的计数器
for (const auto &w : word_count) //对map中的每个元素打印结果
cout << w.first << " occurs " << w.second
<< ((w.second > 1) ? " times" : " time") << endl;
管理桶
无序容器在储存组织为一组桶,每个桶保存零个或多个元素。容器将具有一个特定哈希值的所有的元素都保存在相同的桶中。无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
桶接口
操作 | 描述 |
---|---|
c.bucket_count() | 正在使用的桶的数目 |
c.max_bucket_count() | 容器能容纳的最多的桶的数量 |
c.bucket_size(n) | 第n个桶中有多少个元素 |
c.bucket(k) | 关键字为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.rehash(n) | 重组储存,使得bucket_count >= n且 bucket_count >size/max_load_factor |
c.reserve(n) | 重组储存,使得c可以保存n个元素气人不必rehash |
可以直接定义关键字是内置类型(包括指针类型)、string还是智能指针类型的无序容器。也可以自定义hash比较函数,示例如下:
size_t hasher (const Sales_data &sd)
{
return hash<string>() (sd.isbn());
}
bool eqOp (const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn();
}
//使用上述函数来定义一个unordered_multiset
using SD_multiset = unordered_multiset<Sales_data,
decltype(hasher)*, decltype(eqOp)*>;
//参数是桶大小。哈希函数指针和相等性判断运算符指针
SD_multiset bookstore(42, hasher, eqOp);