《C++ Primer》笔记 第11章 关联容器
关联容器类型 | 解释 |
---|---|
按关键字有序保存元素 | —— |
map | 关联数组;保存关键字-值对 |
set | 关键字即值,即只保存关键字的容器 |
multimap | 关键字可重复出现的map |
multiset | 关键字可重复出现的set |
无序集合 | —— |
unordered_map | 用哈希函数组织的map |
unordered_set | 用哈希函数组织的set |
unordered_multimap | 哈希组织的map;关键字可以重复出现 |
unordered_multiset | 哈希组织的set;关键字可以重复出现 |
类型
map
和multimap
定义在头文件map
中;set
和multiset
定义在头文件set
中;无序容器则定义在头文件unordered_map
和unordered_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的计数器
关联容器不支持顺序容器的位置相关操作,例如
push_front
或push_back
。原因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。关联容器的迭代器都是双向的。
定义关联容器。当初始化一个map时,必须提供关键字类型和值类型。我们将每个关键字-值对包围在花括号中:
{key, value}
来指出它们一起构成了map中的一个元素。在每个花括号中,关键字是第一个元素,值是第二个。map<string, size_t> word_count; // 空容器 // 列表初始化 set<string> exclude = {"The", "But", "And", "Or", "An", "A", "the", "but", "and", "or", "an", "a"}; // 三个元素;authors将姓映射为名 map<string, string> authors = { {"Joyce", "James"}, {"Austen", "Jane"}, {"Dickens", "Charles"} }; // 定义一个有20个元素的vector vector<int> ivec = {0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9}; // iset包含来自ivec的不重复的10个元素;miset包含所有20个元素 set<int> iset(ivec.cbegin(), ivec.cend()); multiset<int> miset(ivec.cbegin(), ivec.cend());
对有序容器——map、multimap、set以及multiset,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。在集合类型中,关键字类型就是元素类型;在映射类型中,关键字类型是元素的第一部分的类型。
传递给排序算法的可调用对象必须满足与关联容器中关键字一样的类型要求。
可以向一个算法提供我们自定义的比较操作,与之类似,也可以提供自己定义的操作来替代关键字上的<运算符。所提供的操作必须在关键字类型上定义一个严格弱序。可以将严格弱序看作“小于等于”,虽然实际定义的操作可能是一个复杂的函数。无论我们怎样定义比较函数,它必须具备如下基本性质:
- 两个关键字不能同时“小于等于”对方;如果k1“小于等于”k2,那么k2绝对不能“小于等于”k1。
- 如果k1“小于等于”k2,且k2“小于等于”k3,那么k1必须“小于等于”k3。
- 如果存在两个关键字,任何一个都不“小于等于”另一个,那么我们称这两个关键字是“等价”的。如果k1“等价于”k2,且k2“等价于”k3,那么k1必须“等价于”k3。
在实际编程中,重要的是,如果一个类型定义了“行为正常”的<运算符,则它可以用作关键字类型。
用来组织一个容器中元素的操作的类型也是该容器类型的一部分。为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型。
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) { return lhs.isbn() < rhs.isbn(); } // bookstore中多条记录可以相同的ISBN // 用compareIsbn来初始化bookstore对象,这表示我们向bookstore添加元素时,通过调用compareIsbn来为这些元素排序 // 即,bookstore中的元素以ISBN的顺序进行排列 multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
pair的标准库类型,定义在头文件utility中。一个pair保存两个数据成员。当创建一个pair时,我们必须提供两个类型名,pair的数据成员将具有对应的类型。两个类型不要求一样。
pair的默认构造函数对数据成员进行值初始化。也可以为每个成员提供初始化器:
pair<string, string> anon; // anon是一个包含两个空string的pair pair<string, string> author{"James", "Joyce"};
与其他标准库类型不同,pair的数据成员是public的,两个成员分别命名为first和second。我们用普通的成员访问符号来访问他们。
map的元素是pair。
pair上的操作 | 解释 |
---|---|
pair<T1, T2> p; | p是一个pair,两个类型分别为T1和T2的成员都进行了值初始化 |
pair<T1, T2> p(v1, v2) | p是一个成员类型为T1和T2的pair;first和second成员分别用v1和v2进行初始化 |
pair<T1, T2> p = {v1, v2}; | 等价于p(v1, v2) |
make_pair(v1, v2) | 返回一个用v1和v2初始化的pair。pair的类型从v1和v2的类型推断出来 |
p.first | 返回p的名为first的(公有)数据成员 |
p.second | 返回p的名为second的(公有)数据成员 |
p1 relop p2 | 关系运算符(<、>、<=、>=)按字典序定义:例如,当p1.first < p2.first 或!(p2.first < p1.first) && p1.second < p2.second 成立时,p1 < p2 为true 。关系运算符利用元素的<运算符来实现 |
p1 == p2或p1 != p2 | 当first和second成员分别相等时,两个pair相等。相等性判断利用元素的==运算符实现 |
pair<string, int> process(vector<string> &v)
{
// 处理v
if (!v.empty())
return {v.back(), v.back().size()}; // 列表初始化,下述等价
// 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类型,key_type和value_type是一样的;set中保存的值就是关键字。在一个map中,元素是关键字-值对。即,每个元素是一个pair对象,包含一个关键字和一个关联的值。由于我们不能改变一个元素的关键字,因此这些pair的关键字部分是const的。
set<string>::value_type v1; // v1是一个string set<string>::key_type v2; // v2是一个string map<string, int>::value_type v3; // v3是一个pair<const string, int> map<string, int>::key_type v4; // v4是一个string map<string, int>::mapped_type v5; // v5是一个int
当解引用一个关联容器迭代器时,我们会得到一个类型为容器的value_type的值的引用。对map而言,value_type是一个pair类型,其first成员保存const的关键字,second成员保存值。
// 获得指向word_count中一个元素的迭代器 auto map_it = word_count.begin(); // *map_it是指向一个pair<const string, size_t>对象的引用 cout << map_it->first; //打印此元素的关键字 cout << " " << map_it->second; // 打印此元素的值 map_it->first = "new key"; // 错误:关键字是const的 ++map_it->second; // 正确:我们可以通过迭代器改变元素
虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素。与不能改变一个map元素的关键字一样,一个set中的关键字也是const的。可以用一个set迭代器来读取元素的值,但不能修改:
set<int> iset = {0,1,2,3,4,5,6,7,8,9}; set<int>::iterator set_it = iset.begin(); if (set_it != iset.end()) { *set_it = 42; // 错误:set中的关键字是只读的 cout << *set_it << endl; // 正确:可以读关键字 }
当使用一个迭代器遍历一个map、multimap、set或multiset时,迭代器按关键字升序遍历元素。
// 获得一个指向首元素的迭代器 auto map_it = word_count.cbegin(); // 比较当前迭代器和尾后迭代器 while (map_it != word_count.cend()) { // 解引用迭代器,打印关键字-值对 cout << map_it->first << " occurs " << map_it->second << " times" << endl; ++map_it; // 递增迭代器,移动到下一个元素 }
关联容器的insert成员向容器中添加一个元素或一个元素范围。由于map和set包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响。
insert有两个版本,分别接受一对迭代器,或是一个初始化器列表,这两个版本的行为类似对应的构造函数(对于一个给定的关键字,只有第一个带此关键字的元素才被插入到容器中)。
对一个map进行insert操作时,必须记住元素类型是pair:
// 向word_count插入word的4种方法 word_count.insert({word, 1}); word_count.insert(make_pair(word, 1)); word_count.insert(pair<string, size_t>(word, 1)); word_count.insert(map<string, size_t>::value_type(word, 1));
关联容器insert操作 | 解释 |
---|---|
c.insert(v)或c.emplace(args) | v是value_type类型的对象;args用来构造一个元素。对于map和set,只有当元素的关键字不在c中时才插入(或构造)元素。函数返回一个pair,包含一个迭代器,指向具有指定关键字的元素,以及一个指示插入是否成功的bool值。对于multimap和multiset,总会插入(或构造)给定元素,并返回一个指向新元素的迭代器。 |
c.insert(b, e)或c.insert(il) | b和e是迭代器,表示一个c::value_type类型值的范围;il是这种值的花括号列表。函数返回void。对于map和set,只插入关键字不在c中的元素。对于multimap和multiset,则会插入范围中的每个元素。 |
c.insert(p, v)或c.emplace(p, args) | 类似insert(v)(或emplace(args)),但将迭代器p作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素。 |
从关联容器删除元素 | 解释 |
---|---|
c.erase(k) | 从c中删除每个关键字为k的元素。返回一个size_type值,指出删除的元素的数量 |
c.erase(p) | 从c中删除迭代器p指定的元素。p必须指向c中一个真实元素,不能等于c.end()。返回一个指向p之后元素的迭代器,若p指向c中的尾元素,则返回c.end() |
c.erase(b,e) | 删除迭代器对b和e所表示的范围中的元素。返回e |
map和unordered_map的下标操作 | 解释 |
---|---|
c[k] | 返回关键字为k的元素;如果k不在c中,添加一个关键字为k的元素,对其进行值初始化 |
c.at(k) | 访问关键字为k的元素,带参数检查;若k不在c中,抛出一个out_of_range 异常 |
map和unordered_map容器提供了下标运算符和一个对应的at函数。
set类型不支持下标,因为set中没有与关键字相关联的“值”。
我们不能对一个multimap或一个unordered_multimap进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。
如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化。
由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作。
通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。但对map则不然:当对一个map进行下标操作时,会获得一个mapped_type对象;但当解引用一个map迭代器时,会得到一个value_type对象。
与其他下标运算符相同的是,map的下标运算符返回一个左值。
另一方面,有时只是想知道一个元素是否已在map中,但在不存在时并不想添加元素。在这种情况下,就不能使用下标运算符,应该使用find:
if (word_count.find("foobar") == word_count.end()) cout << "foobar is not in the map" << endl;
在一个关联容器中查找元素的操作 | 解释 |
---|---|
—— | 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或multiset中查找元素:
string search_item("Alain de Botton"); // 要查找的作者 auto entries = authors.count(search_item); // 元素的数量 auto iter = authors.find(search_item); // 此作者的第一本书 // 用一个循环查找此作者的所有著作 while (entries) { cout << iter->second << endl; // 打印每个题目 ++iter; // 前进到下一本书 --entries; // 记录已经打印了多少本书 } // 当我们遍历一个multimap或multiset时,保证可以得到序列中所有具有给定关键字的元素
用相同的关键字调用lower_bound和upper_bound会得到一个迭代器范围,表示所有具有该关键字的元素的范围。如果关键字在容器中,lower_bound返回的迭代器将指向第一个具有给定关键字的元素,而upper_bound返回的迭代器则指向最后一个匹配给定关键字的元素之后的位置。如果元素不在multimap或multiset中,则lower_bound和upper_bound会返回相等的迭代器——指向一个不影响排序的关键字插入位置。
// authors和search_item的定义,与前面的程序一样 // beg和end表示对应此作者的元素的范围 for (auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item); beg != end; ++beg) cout << beg->second << endl; // 打印每个题目 // lower_bound返回的迭代器可能指向一个具有给定关键字的元素,但也可能不指向。 // 如果关键字不在容器中,则lower_bound会返回关键字的第一个安全插入点——不影响容器中元素顺序的插入位置。
直接调用equal_range:此函数接受一个关键字,返回一个迭代器pair。若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。
// authors和search_item的定义,与前面的程序一样 // pos保存迭代器对,表示与关键字匹配的元素范围 for (auto pos = authors.equal_range(search_item); pos.first != pos.second; ++pos.first) cout << pos.first->second << endl; // 打印每个题目 // 此程序本质上与前一个使用upper_bound和lower_bound的程序是一样的。
综合应用(一个单词转换的map):
#include <map>
#include <vector>
#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
#include <sstream>
using std::cout;
using std::endl;
using std::getline;
using std::ifstream;
using std::istringstream;
using std::map;
using std::runtime_error;
using std::string;
using std::vector;
map<string, string> buildMap(ifstream &map_file)
{
map<string, string> trans_map; // holds the transformations
string key; // a word to transform
string value; // phrase to use instead
// read the first word into key and the rest of the line into value
while (map_file >> key && getline(map_file, value))
{
if (value.size() > 1) // check that there is a transformation
{
trans_map[key] = value.substr(1); // skip leading space
}
else
{
throw runtime_error("no rule for " + key);
}
}
return trans_map;
}
const string &transform(const string &s, const map<string, string> &m)
{
// the actual map work; this part is the heart of the program
auto map_it = m.find(s);
// if this word is in the transformation map
if (map_it != m.cend())
{
return map_it->second; // use the replacement word
}
else
{
return s; // otherwise return the original unchanged
}
}
// first argument is the transformations file;
// second is file to transform
void word_transform(ifstream &map_file, ifstream &input)
{
auto trans_map = buildMap(map_file); // store the transformations
// for debugging purposes print the map after its built
cout << "Here is our transformation map: \n\n";
for (auto entry : trans_map)
{
cout << "key: " << entry.first << "\tvalue: " << entry.second << endl;
}
cout << "\n\n";
// do the transformation of the given text
string text; // hold each line from the input
while (getline(input, text)) // read a line of input
{
istringstream stream(text); // read each word
string word;
bool firstword = true; // controls whether a space is printed
while (stream >> word)
{
if (firstword)
{
firstword = false;
}
else
{
cout << " "; // print a space between words
}
// transform returns its first argument or its transformation
cout << transform(word, trans_map); // print the output
}
cout << endl; // done with this line of input
}
}
int main(int argc, char **argv)
{
// open and check both files
if (argc != 3)
{
throw runtime_error("wrong number of arguments");
}
ifstream map_file(argv[1]); // open transformation file
if (!map_file) // check that open succeeded
{
throw runtime_error("no transformation file");
}
ifstream input(argv[2]); // open file of text to transform
if (!input) // check that open succeeded
{
throw runtime_error("no input file");
}
word_transform(map_file, input);
return 0; // exiting main will automatically close the files
}
- 管理桶:无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
无序容器管理操作 | 解释 |
---|---|
桶接口 | —— |
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会在需要时添加新的桶,以使得load_factor<=max_load_factor |
c.rehash(n) | 重组存储,使得bucket_count>=n且bucket_count>size/max_load_factor |
c.reserve(n) | 重组存储,使得c可以保存n个元素且不必rehash |
默认情况下,无序容器使用关键字类型的==运算符来比较元素,它们还使用一个
hash<key_type>
类型的对象来生成每个元素的哈希值。标准库为内置类型(包括指针)提供了hash模板。还为一些标准库类型,包括string和智能指针类型定义了hash。因此,我们可以直接定义关键字是内置类型(包括指针类型)、string还是智能指针类型的无序容器。但是,我们不能直接定义关键字类型为自定义类类型的无序容器。我们需要提供函数来替代==运算符和哈希值计算函数。
size_t hasher(const Sales_data &sd) { // 我们的hasher函数使用一个标准库hash类型对象来计算ISBN成员的哈希值 // 该hash类型建立在string类型之上 return hash<string>()(sd.isbn()); } bool eqOp(const Sales_data &lhs, const Sales_data &rhs) { // eqOp函数通过比较ISBN号来比较两个Sales_data return lhs.isbn() == rhs.isbn(); } using SD_multiset = unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>; // 参数是桶大小、哈希函数指针和相等性判断运算符指针 SD_multiset bookstore(42, hasher, eqOp);
如果我们的类定义了==运算符,则可以只重载哈希函数。
// 使用FooHash生成哈希值;Foo必须有==运算符 unordered_set<Foo, decltype(FooHash)*> fooSet(10, FooHash);
有序容器使用比较函数来比较关键字,从而将元素按顺序存储。默认情况下,比较操作是采用关键字类型的<运算符。无序容器使用关键字类型的==运算符和一个
hash<key_type>
类型的对象来组织元素。