文章目录
- 关联容器
- 11.1使用关联容器
- 使用map
- 使用set
- 11.2关联容器概述
- 11.2.1定义关联容器
- 初始化multimap或multiset
- 11.2.2关键字类型的要求
- 有序容器的关键字类型
- 使用关键字类型的比较函数
- 11.2.3pair类型
- 创建pair对象的函数
- 11.3关联容器操作
- 11.3.1关联容器迭代器
- set的迭代器是const的
- 遍历关联容器
- 关联容器和算法
- 11.3.2添加元素
- 向set添加元素
- 向map添加元素
- 检测insert的返回值
- 向multiset或multimap添加元素
- 11.3.3删除元素
- 11.3.4map的下标操作
- 11.3.5访问元素
- 在multimap或multiset中查找元素
- 一种不同的,面向迭代器的解决办法
- equal_range函数
- 11.4无序容器
- 管理桶
- 无序容器对关键字类型的要求
关联容器
类型
map
和multimap
定义在头文件map
中;set
和multiset
定义在头文件set
中;无序容器则定义在头文件unordered_map
和unordered_set
中。
11.1使用关联容器
使用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中的每个元素
// 打印结果
cout << w.first << " occurs " << w.second
<< ((w.second > 1) ? " times" : " time") << endl;
}
当从
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];
}
}
11.2关联容器概述
关联容器都支持普通容器操作:
但是不支持顺序容器的位置相关的操作,例如
push_front
或push_back
。原因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。而且,关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。
关联容器的迭代器都是双向的。
11.2.1定义关联容器
map<string, size_t> word_count;
set<string> exclude = {"The", "But", "And", "Or", "An", "A",
"the", "but", "and", "or", "an", "a"};
map<string, string> authors = {
{"Joyce", "James"},
{"Austen", "Jane"},
{"Dickens", "Charles"}
};
初始化multimap或multiset
multimap
或multiset
都允许多个元素具有相同的关键字。
// 定义一个有20个元素的vector,保存0到9每个整数的两个拷贝。
vector<int> ivec;
for (vector<int>::size_type i = 0; i != 10; ++i) {
ivec.push_back(i);
ivec.push_back(i); // 每个数重复保存一次
}
// iset包含来自ivec的不重复的元素;miset包含所有20个元素。
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
11.2.2关键字类型的要求
对于有序容器,
map
、multimap
、set
以及multiset
,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<
运算符来比较两个关键字。
有序容器的关键字类型
可以提供自己定义的操作来代替关键字上的
<
运算符。所提供的操作必须在关键字类型上定义一个严格弱序。可以将严格弱序看作小于等于。
在实际编程中,重要的是,如果一个类型定义了行为正常的<
运算符,则它可以用作关键字类型。
使用关键字类型的比较函数
用来组织一个容器中元素的操作的类型也是该容器类型的一部分。为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型。
在尖括号中出现的每个类型,就仅仅是一个类型而已。当创建一个容器(对象)时,才会以构造函数参数的形式提供真正的比较操作(其类型必须与在尖括号中指定的类型相吻合)。
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {
return lhs.isbn() < rhs.isbn();
}
// decltype必须加上一个*来指出要使用一个给定函数类型的指针
multiset<Sales_data, decltype(compareIsbn) *> bookstore(compareIsbn);
11.2.3pair类型
定义在头文件
utility
中。pair
的默认构造函数对数据成员进行值初始化。
// 包含两个空string
pair<string, string> anon;
// string为空,size_t为0。
pair<string, size_t> word_count;
// 保存一个空string和一个空vector
pair<string, vector<int>> line;
pair<string, string> author{"James", "Joyce"};
创建pair对象的函数
// 新标准。较早的c++版本中,不允许用花括号包围的初始化器来返回pair这种类型的对象,必须显式构造返回值。
// 还可以用make_pair来生成pair对象。
pair<string, int> process(vector<string> &v) {
// 处理v
if (!v.empty()) {
// 旧标准
// return pair<string, int>(v.back(), v.back().size());
// make_pair
// return make_pair(v.back(), v.back().size());
return {v.back(), v.back().size()}; // 列表初始化
} else {
return pair<string, int>(); // 隐式构造返回值
}
}
11.3关联容器操作
set<string>::value_type v1; // v1是一个string
set<string>::key_type v2; // v2是一个string
// 由于不能改变一个元素的关键字,因此pair的关键字部分是const的。
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
11.3.1关联容器迭代器
当解引用一个关联容器迭代器时,会得到一个类型为容器的
value_type
的值的引用。
// 获得指向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的迭代器是const的
虽然
set
同时定义了iterator
和const_iterator
类型,但两种类型都只允许访问set
中的元素。一个set
中的关键字也是const
的。
遍历关联容器
当使用一个迭代器遍历一个
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; // 递增迭代器,移动到下一个元素。
}
关联容器和算法
通常不对关联容器使用泛型算法。关键字是
const
的特性意味着不能将关联容器传递给修改或重排容器元素的算法,因为这类算法需要向元素写入值,而set
类型中的元素是const
的,map
中的元素是pair
,其第一个成员是const
的。
关联容器可用于只读取元素的算法。但是,很多这类算法都要搜索序列。由于关联容器中的元素不能通过它们的关键字进行(快速)查找,因此对其使用泛型搜索算法几乎总是个坏主意。通常情况下,使用其find
成员会好很多。
在实际编程中,如果真要对一个关联容器使用算法,要么是将它当作一个源序列,要么当作一个目的位置。例如,可以用泛型copy
算法将元素从一个关联容器拷贝到另一个序列。类似的,可以调用inserter
将一个插入器绑定到一个关联容器。通过使用inserter
,可以将关联容器当作一个目的位置来调用另一个算法。
11.3.2添加元素
向set添加元素
vector<int> ivec = {2, 4, 6, 8, 2, 4, 6, 8};
set<int> set2;
set2.insert(ivec.cbegin(), ivec.cend()); // set2有4个元素
set2.insert({1, 3, 5, 7, 1, 3, 5, 7}); // set2现在有8个元素
向map添加元素
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的返回值
// 统计每个单词在输入中出现次数的一种更烦琐的方法
map<string, size_t> word_count; // 从string到size_t的空map
string word;
while (cin >> word) {
// 插入一个元素,关键字等于word,值为1;
// 若word已在word_count中,insert什么也不做。
// 旧版本的编译器,ret的声明和初始化为:pair<map<string, size_t>::iterator, bool> ret = ...
auto ret = word_count.insert({word, 1});
if (!ret.second) {
++ret.first->second;
}
}
向multiset或multimap添加元素
一个
multi
容器中的关键字不必唯一,在这些类型上调用insert
总会插入一个元素:
multimap<string, string> authors;
// 插入第一个元素,关键字为Barth, John
aothors.insert({"Barth, John", "Sot-Weed Factor"});
// 正确:添加第二个元素,关键字也是Barth, John
aothors.insert({"Barth, John", "Lost in the Funhouse"});
11.3.3删除元素
11.3.4map的下标操作
set
不支持下标,因为set
中没有与关键字相关联的值,元素本身就是关键字。不能对一个multimap
或一个unorderd_multimap
进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。
由于下标运算符可能插入一个新元素,只可以对非
const
的map
使用下标操作。
需要注意的是,通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。但对map
进行下标操作时,会获得一个mapped_type
对象;但当解引用一个map
迭代器时,会得到一个value_type
对象。
11.3.5访问元素
在multimap或multiset中查找元素
如果一个
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;
}
一种不同的,面向迭代器的解决办法
lower_bound
返回的迭代器指向第一个具有给定关键字的元素,而upper_bound
返回的迭代器则指向最后一个匹配到的给定关键字的元素之后的位置。如果元素不存在,则lower_bound
和upper_bound
会返回相等的迭代器,指向一个不影响排序的关键字插入位置。
如果查找的元素具有容器中最大的关键字,则此关键字的upper_bound
返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则lower_bound
返回的也是尾后迭代器。
for (auto beg = authors.lower_bound(search_item),
end = authors.upper_bound(search_item);
beg != end; ++beg) {
cout << beg->second << endl;
}
equal_range函数
equal_range
返回一个迭代器pair
。若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。
for (auto pos = authors.equal_range(search_item);
pos.first != pos.second; ++pos.first) {
cout << pos.first->second << endl;
}
11.4无序容器
无序容器使用一个哈希函数和关键字类型的
==
运算符。
管理桶
无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的。当一个桶保存多个元素时,需要顺序搜索这些元素来查找想要的那个。计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定元素就需要大量的比较操作。
无序容器对关键字类型的要求
默认情况下,无序容器使用关键字类型的
==
运算符来比较元素,它们还使用一个hash<key_type>
类型的对象来生成每个元素的哈希值。标准库为内置类型(包括指针)提供了hash
模板。还为一些标准库类型,包括string
和智能指针类型定义了hash
。因此,可以直接定义关键字是这些类型的无序容器。
但是,不能直接定义关键字类型为自定义类类型的无序容器,必须提供自己的hash
模板版本。
不使用默认的hash
,而是使用另一种方法,类似于为有序容器重载关键字类型的默认比较操作。
// 为了能将Sales_data用作关键字,需要提供函数来替代==运算符和哈希值计算函数。
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 = unorderd_multiset<Sales_data, decltype(hasher) *, decltype(eqOp) *>;
// 参数是桶大小、哈希函数指针和相等性判断运算符指针
SD_multiset bookstore(42, hasher, eqOp);
// 如果定义了==运算符,则可以只重载哈希函数:
unordered_set<Foo, decltype(FooHash) *> fooSet(10, FooHash);