第十一章 关联容器
使用关联容器
set支持高效的关键字查询操作——检查一个给定的关键字是否在set中
标准库提供8个关联容器,不同表现在三个维度上,每个容器:
- 或者是一个set,或者是一个map
- 或者要求不重复关键字,或者允许重复关键字
允许重复关键字的容器的名字包含单词multi - 按顺序保存元素,或无序保存
不保持关键字按顺序存储的容器的名字都以单词unordered开头
unordered_multi_set是一个允许重复关键字,元素无序保存的集合
类型map和multimap定义在头文件map中
类型set和multiset定义在头文件set中
无序容器定义在头文件unordered_map和unordered_set中
关联容器类型
- map
- set
- multimap
- multiset
- unordered_map
- unordered_set
- unordered_multiset
- unordered_multimap
map<string, size_t> word_count;
string word;
while (cin >> word)
++word_count[word];
for (const auto &w : word_count)
cout << w.first << " " << w.second << endl;
如果关键字未在map中,下标运算符会创建一个新元素
关联容器概述
关联容器都支持第九章中介绍的普通容器的操作,不支持顺序容器的位置相关的操作,原因是关联容器中元素是根据关键字存储的,位置对于关联容器没有意义。而且不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作
除了与顺序容器的相同操作外,关联容器还支持一些顺序容器不支持的操作和类型别名。此外,无序容器还提供一些用来调整哈希性能的操作。关联容器的迭代器都是双向的
定义关联容器
- 默认初始化空容器
- 列表初始化
- 同类型其他容器初始化
- 迭代器范围初始化
-
map<string, size_t> word_count; // 空容器 set<string> exclude = {"the", "but", "and", "or", "an", "a"}; // 列表初始化 map<string, string> authors = {{"a", "A"}, {"b", "B"}}; //值初始化
-
vector<int> ivec; for (vector<int>::size_type i = 0; i != 10; ++i) { ivec.push_back(i); ivec.push_back(i); } set<int> iset(ivec.cbegin(), ivec.cend()); multiset<int> miset(ivec.cbegin(), ivec.cend()); cout << ivec.size() << endl; // 20 cout << iset.size() << endl; // 10 cout << miset.size() << endl; // 20
关键字类型的要求
-
关键字:map的第一个,set的类型
-
对于有序容器,关键字类型必须元素比较的方法。默认情况下,标准库使用关键字类型的<运算符来比较两个关键字
-
可以向一个算法提供我们自己定义的比较操作,与之类似,也可以提供自己定义的操作来代替关键字上的<运算符。所提供的操作必须在关键字类型上定义一个严格弱序
-
严格弱序具备的基本性质
- 两个关键字不能同时“小于等于”对方,且小于等于具有传递性
- 如果两个关键字任何一个都不小于等于另一个,则说两个关键字等价,等价具有传递性
-
在实际编程中,重要的是,如果一个类似定义了“行为正常”的<运算符,则它可以用作关键字类型
-
// 自定义操作,来组织容器的顺序 decltype(cmp) *a = cmp; bool (*b)(const string &, const string &); b = cmp; multiset<string, decltype(cmp) *> bookstore(cmp);
pair类型
-
定义在utility头文件中
-
默认构造函数进行值初始化
-
pair的数据成员是public的
-
操作
- pair<T1,T2> p
- pair<T1,T2> p(v1,v2)
- pair<T1,T2> p = {v1,v2}
- make_pair(v1,v2)
- p.first
- p1 关系运算符 p2
- p1==p2
- p1!=p2
-
pair<string, int> process(vector &v)
{
if (!v.empty())
return {v.back(), v.back().size()}; // 列表初始化
// 较早的C++版本中,显式构造返回值与利用make_pair函数返回值
// return pair<string, int>(v.back(), v.back().size());
// return make_pair(v.back(), v.back().size());
else
return pair<string, int>(); // 隐式构造返回值
}
关联容器操作
关联容器额外的类型别名
- key_type
此容器类型的关键字类型 - mapped_type
每个关键字关联的类型:只适用于map - value_type
对于set,与key_type相同
对于map,为pair<const key_type, mapped_type> -
set<string>::value_type v1; // v1是一个string set<string>::key_type v2; // string map<string, int>::value_type v3; // pair<const string,int> map<string, int>::key_type v4; // string map<string, int>::mapped_type v5; //int
关联容器迭代器
- 当解引用一个关联容器迭代器时,得到一个类型为容器的value_type的值的引用
-
map<string, int> word; auto map_it = word.begin(); cout << map_it->first; //打印关键字 cout << " " << map_it->second; // map_it->first = "new key"; //错误,关键字是const的 ++map_it->second; // 正确,可以改变值
- 必须记住一个map的value_type是一个pair,可以改变pair的值,但是不能改变关键字成员的值
- 虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素。与不能改变一个map元素的关键字一样,一个set中的关键字也是const的
-
set<int> iset = {0, 1, 2, 3, 4}; set<int>::iterator set_it = iset.begin(); if (set_it != iset.end()) { // *set_it=42; // 错误:set中的关键字是只读的 }
- 我们通常不对关联容器使用泛型算法,关键字是const这一特性意味着不能将关联容器传递给修改或重排容器元素的算法,因为这类算法需要向元素写入值,而set类型是const的,map中的元素是pair,其中一个成员是const
关联容器可用于只读算法,但是,这类算法大部分都要搜索序列。由于关联容器中的元素不能通过它们的关键字进行快速查找,因此几乎使用泛型搜索算法几乎都是坏主意
实际编程中,如果我们真的要对一个关联容器使用算法, 要么当做一个目的位置,要么当做原序列。
例如使用copy算法调用插入器添加元素
添加元素
-
插入一个已存在的元素对容器没有任何影响
-
关联容器insert操作
- c.insert(v)
c.emplace(args)
v是value_type类型对象,args用来构造一个元素
对于map和set,只有当元素的关键字不在容器中时才插入或构造元素。
返回一个pair,包含一个迭代器指向指定关键字以及一个是否插入的bool值
对于multi容器,因为总会插入元素,所以只返回一个指向新元素的迭代器 - c.insert(b,e)
c.insert(il)
il是花括号列表
只插入范围内的容器中不存在的元素 - c.insert(p,v)
c.emplace(p,args)
迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置,返回一个迭代器,指向具有给定关键字的元素
- c.insert(v)
-
vector<int> ivec = {2, 4, 6, 8, 2, 4, 6, 8}; set<int> set2; set2.insert(ivec.begin(), ivec.end()); // 4个元素 set2.insert({1, 3, 5, 7, 1, 3, 5, 7}); // 现在有8个元素
-
// 向word_count中插入数据的4种方法 map<string, int> word_count; string word; word_count.insert({word, 1}); word_count.insert(make_pair(word, 1)); word_count.insert(pair<string, int>(word, 1)); word_count.insert(map<string, int>::value_type(word, 1));
-
// 更加繁琐的一种统计单词次数的写法 map<string, size_t> word_count; string word; while (cin >> word) { // ret为pair<map<string, size_t>::iterator, bool> auto ret = word_count.insert({word, 1}); if (!ret.second) // word已经在容器中 ++ret.first->second; }
删除元素
-
map<string, size_t> word_count; string word; if (word_count.erase(word)) cout << word << " removed\n"; else cout << word << " not found\n";
-
从关联容器删除元素
- c.erase(k)
返回一个size_type值,指出删除元素的数量 - c.erase§
c.erase(b,e)
返回一个指向p之后元素的迭代器
返回e
- c.erase(k)
map下标操作
-
类似我们用过的其他下标运算符,map的下标运算符接受一个索引,获取与此关键字相关联的值,但是与其他下标运算符不同的是,如果关键字并不在map中会为它创建一个元素并插入到map中,关联值进行值初始化
-
map和unordered_map的下标操作
- c[k]
返回关联字k的元素,如果k不在c中,添加一个关键字为k的元素,对其进行值初始化 - c.at(k)
访问关键字为k的元素,不在抛出out_of_range异常
- c[k]
-
当对一个map进行下标操作时,会获得一个mapped_type对象,但当节引用map容器时,会得到一个value_type对象。但是通常情况下下标与解引用迭代器的类型是相同的
访问元素
-
在一个关联容器中查找元素的操作
- lower_bound和upper_bound不适用于无序容器
- 下标和at操作只适用于非const的map和unordered_map
- 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()
-
multimap<string, string> authors; string search_item; auto cnt = authors.count(search_item); // 元素的数量 auto iter = authors.find(search_item); // 此作者的第一本书 while (cnt) { cout << iter->second << endl; // 输出题目 ++iter; // 因为相同关键字的元素相邻 --cnt; } // 在功能上与上面等价于 for (auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item); beg != end; ++beg) cout << beg->second << endl; // 等价于 for (auto pos = authors.equal_range(search_item); pos.first != pos.second; ++pos.first) cout << pos.first->second << endl;
一个单词转换的map
- map<string, string> buildMap(ifstream &map_file)
{
map<string, string> trans_map;
string key;
string value;
// 读取第一个单词存入key,行内剩余内容存储value
while (map_file >> key && getline(map_file, value))
{
// 检查是否有转换规则,跳过前面的空格
if (value.size() > 1)
trans_map[key] = value.substr(1);
else
throw runtime_error(“no rule for” + key);
}
return trans_map;
}
const string &transform(string &s, const map<string, string> &m)
{
auto map_it = m.find(s);
if (map_it != m.cend())
return map_it->second;
else
return s;
}
void word_transform(ifstream &map_file, ifstream &input)
{
auto trans_map = buildMap(map_file);
string text;
while (getline(input, text))
{
istringstream stream(text);
string word;
bool firstword = true;
while (stream >> word)
{
if (firstword)
firstword = false;
else
cout << " ";
cout << transform(word, trans_map);
}
cout << endl;
}
}
无序容器
新标准定义了4个无序关联容器,这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字类型的==运算符。在关键字类型的元素没有明显的序关系的情况下,无序容器时非常有用的。在某些应用中,维护元素的序代价非常高昂,此时无序容器很有用
如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器
通常可以用一个无序容器替换对应的有序容器,反之亦然。但是两种容器的输出通常不同
无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射为桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在一个桶 中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小
当一个桶保存多个元素时,需要顺序搜索这些元素来查找我们想要的那个。计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定的元素就需要大量比较操作
无序容器提供了一组管理桶的函数,允许我们查询容器的状态以及在必要的时候强制容器进行重组
默认情况下,无序容器使用关键字类型的==运算符来比较元素,它们还使用一个hash<key_type>类型的对象生成每个元素的哈希值。标准库为内置类型提供了hash模板。还为一些标准库类型定义了hash。但是,我们不能直接定义关键字类型为自定义类类型的无序容器。与容器不同,不能直接使用哈希模板,而必须提供我们自己的hash模板版本
size_t hasher(const Sales_data &sd)
{
return hash()(sd.isbn());
}
bool eqop(const Sales_data &a, const Sales_data *b)
{
return a.isbn() == b.isbn();
}
using SD_multiset = unordered_multiset<Sales_data, decltype(hasher) *, decltype(eqop) *>;
int main(void)
{
SD_multiset bookstore(42, hasher, eqop);
// Foo类有==运算符
unordered_set<Foo, decltype(FooHash) *> fooSet(10, FooHash);
return 0;
}
术语表
1-10
- 关联数组
- 关联容器
- hash
- 哈希函数
- key_type
- map
- mapped_type
- multimap
- multiset
- pair
11-20
- set
- 严格弱序
- 无序容器
- unordered_map
- unordered_multimap
- unordered_multiset
- unordered_set
- value_type
- *运算符
- []运算符
XMind: ZEN - Trial Version