关联容器中的元素是按关键字来保存和访问的,与之相对的顺序容器是按它们在容器中的顺序来保存和访问的。
关联容器支持搞笑的关键字查找和访问,两个主要的关联容器类型是map和set。map中的元素是一些键(关键字)值对,键起到索引的作用,值则表示与索引相关的数据。set中每个元素只包含一个关键字(关键字即值),set支持高效的关键字查询操作。
标准库提供8个关联容器,它们的不同体现在三个不同维度上:
1.或者是一个map,或者是一个set。
2.或者要求不重复的关键字,或者允许重复的关键字。
3.按顺序保存元素或无序保存。
允许重复关键字的容器名字中都包含单词multi,不保持关键字按顺序存储的容器名都以unordered开头,因此unordered_multi_set是一个允许重复关键字、元素无序保存的集合,而set是一个要求不重复关键字、有序存储的集合。
无序容器使用哈希函数来组织元素。
类型map和multimap定义在头文件map中,set和multiset定义在头文件set中。无序容器则定义在头文件unordered_map和unordered_set中。
map可以看成将关键字映射到值。map类型通常被称为关联数组,它的特殊之处在于其下标可以不是整数,可以是关键字,如给定一个名字到电话号码的map,我们可以使用人名作为下标来获取此人的电话号码。
set就是关键字的简单集合。当只是想知道一个值是否存在时,set是最有用的。set的去重是库函数实现的,编写简单,并且查找的效率高,set是红黑树实现的,查找效率与元素个数成对数关系。
map给单词计数:
map<string, int> word_count;
string word;
while (cin >> word) {
++word_count[word]; //如果word未在map中,会创建一个新元素,其关键字为word,值为0
}
for (const auto &w : word_count) {
cout << w.first << " occurs " << w.second << ((w.second > 1) ? "times" : "time") << endl;
}
以上for循环中,当从map中提取一个元素时,会得到一个pair类型的对象,它是一个模板类型,保存两个名为first和second的公有数据成员,first用来保存关键字,而second保存对应的值。
类似于顺序容器,关联容器也是模板,为定义一个map,我们需要指定关键字和值的类型。
上例的扩展:忽略某些的单词,我们可以使用set保存想忽略的单词,只对不出现在集合中的单词统计出现次数:
map<string, int> word_count;
set<string> exclude = { "The", "the" };
string word;
while (cin >> word) {
if (exclude.find(word) == exclude.end()) { //返回一个迭代器,如给定关键字在set中,则迭代器指向该数字,否则,迭代器指向尾后元素
++word_count[word]; //如果word未在map中,会创建一个新元素,其关键字为word,值为0
}
}
for (const auto &w : word_count) {
cout << w.first << " occurs " << w.second << ((w.second > 1) ? "times" : "time") << endl;
}
set也是一个模板,定义set必须指定其元素类型。可以用列表初始化方式初始化set。
关联容器都支持普通容器操作,但不支持顺序容器相关的操作,如push_back、push_front,原因在于关联容器中元素是根据关键字存储的。关联容器也不支持像构造函数或插入操作这些接受一个元素值和一个数量值的操作。
关联容器的迭代器都是双向迭代器。
如上例,C++11中可以使用列表值初始化关联容器:
map<string, string> authors = { { "Joyce", "James" },
{ "Austen", "Jane" } }; //两个元素的map容器,与以往一样,初始化器必须能转换为容器中元素的类型
每个关联容器都定义了一个默认构造函数,它创建一个指定类型的空容器。我们也可以将关联容器初始化为另一个同类型容器的拷贝。
一个map或set中的关键字必须是唯一的。但容器multimap和multiset没有此限制:
int main() {
//创建一个含有0~9的vector,每个元素重复出现两次
vector<int> ivec;
for (int i = 0; i < 10; ++i) {
ivec.push_back(i);
ivec.push_back(i);
}
set<int> iset(ivec.begin(), ivec.end());
multiset<int> miset(ivec.begin(), ivec.end());
cout << ivec.size() << endl; //输出20
cout << iset.size() << endl; //输出10
cout << miset.size() << endl; //输出20
}
上例中,即使我们使用ivec初始化iset,它也只含不重复的元素,而multiset允许重复元素。
关联容器对其关键字类型有一些限制,对有序容器(map、multimap、set、multimap)来说,关键字类型必须定义元素比较的方法,默认情况下,使用<运算符来比较两个关键字。在set中,关键字类型就是元素类型,在map中,关键字类型是元素的第一部分的类型。以上要求就像传递给排序算法的元素类型一样。
我们可以向一个算法提供我们自己定义的比较操作,类似地,也可以提供自己定义的操作来代替关键字上的<运算符。所提供的操作必须在关键字类型上定义一个严格弱序,它必须具备如下性质:
1.两个关键字不能同时小于等于对方:如k1小于等于k2,则k2不能小于等于k1。
2.传递性,若k1<=k2,k2<=k3,则k1<=k3。
3.若存在两个关键字,任何一个都不小于等于另一个,那么称这两个关键字是等价的。若k1等价于k2,且k2等价于k3,则k1等价于k3。
如果两个关键字是等价的,那么容器将它们视作相等来处理,当这两个关键字用作map的关键字时,只能有一个元素与这两个关键字关联,可以用两者中任意一个来访问对应的值。
使用关键字类型的比较函数:如我们不能直接定义一个Sales_data的multiset,因为Sales_data没有<运算符,但我们可以用compareIsbn函数定义一个multiset,此函数就是关键字类型Sales_data的比较函数,此函数在Sales_data对象的ISBN成员上定义了一个严格弱序:
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs) {
return lhs.isbn() < rhs.isbn();
}
为使用自己定义的操作,在定义multiset时必须提供两个类型,一个是关键字类型Sales_data;另一个是比较操作的类型,它是能指向compareIsbn的函数指针类型:
multiset<Sales_data, decltype(compareIsbn) *> bookStore(compareIsbn);
multiset<Sales_data, bool(*) (const Sales_data&, const Sales_data&)> bookStore(compareIsbn); //与上句代码含义相同
如上,我们使用decltype来获取一个函数指针类型时,必须加上一个*来指出我们要使用的是函数类型的指针。之后用compareIsbn来初始化bookStore表示当我们向bookStore添加元素时,调用compareIsbn来为这些元素排序。我们可以使用compareIsbn来代替&compareIsbn作为构造函数的参数,它会自动转化为一个指针,当然,使用&compareIsbn也是一样的。
定义一个map,将单词与行号的list关联,list中保存的是单词所出现的行号:
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <list>
#include <map>
using namespace std;
int main() {
map<string, list<int>> words;
ifstream fin(文件路径);
string line, word;
int lineCount = 0;
while (getline(fin, line)) {
++lineCount;
istringstream sin(line);
while (sin >> word) {
words[word].push_back(lineCount);
}
}
for (auto& p : words) {
cout << p.first << " : ";
list<int>::iterator b = p.second.begin(), e = p.second.end();
while (b != e) {
cout << *b << " ";
++b;
}
cout << endl;
}
}
可以定义一个vector<int>::iterator到int的map,但不能定义list<int>::iterator到int的map,因为有序关联容器要求关键字必须支持<运算符,而list的迭代器是双向迭代器而不是随机访问迭代器,不支持<操作。
pair类型定义在头文件utility中,一个pair保存两个数据成员,类似容器,pair是一个生成特定类型的模板,当创建一个pair时,我们必须提供两个类型名,pair的数据成员将具有对应的类型,这两个类型可以不同。
pair的默认构造函数对数据成员进行值初始化:
pair<string, int> p; //包含一个空串和一个0
也可以为每个成员提供初始化器:
pair<string, string> author{"James", "Joyce"}; //包含两个给定值的串
pair的数据成员是public的,两个成员分别命名为first和second,我们用成员访问符来访问它们。
map的元素是pair。
pair上的操作:
C++11新标准下,若一个函数需要返回一个pair,我们可以对返回值进行列表初始化:
pair<string, int> process(vector<string> &v) {
if (!v.empty()) {
return { v.back(), v.back().size() }; //列表初始化,用v中最后一个string和最后一个string的大小初始化
}
else {
return pair<string, int>(); //隐式构造返回值
}
}
上例中,较老的C++版本不允许花括号包围的初始化列表来返回pair的对象,必须显式构造返回值:
return pair<string, int>(v.back(), v.back().size());
我们还可以使用make_pair来生成pair对象:
return make_pair(v.back(), v.back().size());
关联容器额外的类型别名:
对于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
只有map类型(unordered_map、unordered_multimap、multimap、map)才有mapped_type。
当解引用一个关联容器迭代器时,我们会得到一个类型为容器的value_type的值的引用,对map而言,value_type是一个pair类型,其first成员保存const的关键字,second成员保存值。
auto map_it = word_count.begin(); //获得指向word_count中一个元素的迭代器
map<string, list<int>>::iterator map_it = word_count.begin(); //含义与上句代码相同
cout << map_it->first; //输出此元素的关键字
cout << " " << map_it->second; //输出此元素的值
map_it->first = "new key"; //错误,元素的关键字是const的
++map_it->second; //正确,元素值不是const的
一个map的value_type是一个pair,我们可以改变pair的值,但不能改变关键字成员的值。
虽然set类型同时定义了iterator和const_iterator类型,但两种类型都只允许访问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中的关键字是只读的
cout << *set_it << endl; //正确,可以访问关键字
}
遍历关联容器:
auto map_it = word_count.cbegin();
while (map_it != word_count.cend()) {
cout << map_it->first << " occurs " << map_it->second << ((map_it->second > 1) ? "times" : "time") << endl;
}
++map_it;
}
以上程序是按字典序列排列的,当使用一个迭代器遍历一个map、multimap、set或multiset时,迭代器按关键字升序遍历元素。
通常我们不对关联容器使用泛型算法,关键字是const的这一特性意味着不能将关联容器传递给修改或重排容器元素的算法,因为这类算法需要向元素写入值。
关联容器可用于只读取元素的算法,但是,很多这类算法需要搜索序列,由于关联容器中的元素通过它们的关键字能很快地进行查找,因此对其使用泛型搜索算法几乎总是个坏主意。关联容器定义了一个名为find的成员,它通过一个给定的关键字直接获取元素,但泛型find会顺序搜索容器,而关联容器定义的find成员会快得多。
实际编程中,如果我们真要对一个关联容器使用算法,要么将它当作一个源序列,要么当作一个目的位置。如,可以使用泛型copy算法将元素从一个关联容器拷贝到另一个序列。类似地,可以调用inserter将一个插入器绑定到一个关联容器,就可以将这个插入器用于一个算法。
multiset<string> smset = { "aaa", "bbb", "ccc" };
vector<string> svec = { "ddd", "eee", "fff" };
copy(svec.begin(), svec.end(), inserter(smset, smset.end())); //正确
copy(svec.begin(), svec.end(), back_inserter(smset)); //错误,报错push_back不是multset的成员
copy(smset.begin(), smset.end(), inserter(svec, svec.end())); //正确
copy(smset.begin(), smset.end(), back_inserter(svec)); //正确
关联容器的insert成员向容器插入一个元素或一个元素范围。由于map和set(及其对应的无序类型)不包含重复的关键字,因此插入一个已存在的元素对容器没有任何影响:
vector<int> ivec = { 2, 4, 6, 2, 4, 6 };
set<int> set2; //空集合
set2.insert(ivec.cbegin(), ivec.cend()); //set2有3个元素
set2.insert({ 1, 3, 5, 1, 3, 5 }); //set2现在有6个元素
insert有两个版本,一个版本接受一对迭代器,另一个版本接受一个初始化列表。
对一个map进行inset操作时,元素类型时pair,通常,对于想要插入的数据,并没有一个pair对象,可以在insert参数列表中创建一个pair:
word_count.insert({ word, 1 }); //C++11新标准
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));
关联容器的insert操作:
insert或emplace返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,它的first成员是一个迭代器,指向具有给定关键字的元素,second成员是一个bool值,指出元素是插入成功还是已经存在于容器。如果关键字已在容器中,则insert什么事情也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器中,且bool值为true。
下面是一个更繁琐的重写统计单词输入次数的例子:
map<string, int> word_count;
string word;
while (cin >> word) {
pair<map<string, int>::iterator, bool> ret = word_count.insert({word, 1}); //若使用C++11版本,ret的类型可以使用auto自动推断
if (!ret.second) {
++ret.first->second;
}
}
其中,递增计数器的语句很难理解,它的优先级等价于:
++((ret.first)->second);
使用multimap:
multimap<string, string> authors;
authors.insert({ "Barth, John", "Sot-Weed Factor" });
authors.insert({ "Barth, John", "Lost in the Funhouse" }); //正确,添加了第二个关键字相同的元素
对于允许重复关键字的容器,接受单个元素的insert操作返回一个指向新元素的迭代器,这里无须返回bool值,因为insert总是向这类容器中加入一个新元素。
新的给单词计数的例子:
while (cin >> word) {
++word_count.insert({ word, 0 }).first->second;
}
其中insert返回的pair中的first成员要么是已经存在的关键字相同的元素的迭代器,要么是新添加的值为0的元素的迭代器,再对迭代器指向对象的值增加1。
关联容器定义了三个版本的erase:
对于保存不重复关键字的容器,erase的返回值总是0或1,0表示想要删除的元素并不在容器中。
对于允许重复关键字的容器,删除元素的数量可能大于1。
map和unordered_map的下标操作:
map和unordered_map容器都提供了下标运算符和一个对应的at函数,但set类型不支持下标,因为set中没有与关键字相关联的值。我们不能对一个multimap或unordered_multimap进行下标操作,因为这些容器中可能有多个值以一个关键字相关联。
对map使用下标运算符时,若下标运算符接受的关键字值不在map中,会为它创建一个关键字值相等的元素并插入map,这个关键字关联的值会被值初始化:
map<string, size_t> word_count; //empty map
word_count["Anna"] = 1;
上例会:
1.在word_count中查找关键字Anna,未找到。
2.将一个新的关键字-值对插入到word_count中,关键字是一个const string,保存Anna,并进行值初始化为0.
3.提取出新插入的元素,并将值1赋给它。
由于下标运算符可能会插入一个新元素,我们只能对非const的map容器使用下标操作。
map的下标运算符与我们用过的其他下标运算符不同之处在于返回类型,通常,解引用一个迭代器返回的类型与下标运算符返回的类型是一样的,但对map进行下标操作时,会获得一个mapped_type类型的对象,而当解引用一个map迭代器时会得到一个value_type类型的对象。
而与其他下标运算符相同的是,map的下标运算符返回的是一个左值。
在一个关联容器中查找元素的操作:
上图中有一个错误,c.equal_range(k),若k不存在,pair的两个成员等于一个安全位置,在此插入关键字值可以不破坏c的顺序:
multimap<string, int> mmap = { { "c", 2 }, { "c", 2} };
auto p = mmap.equal_range("a");
if (p.first == mmap.begin()) {
cout << "若找不到时,指向安全位置。" << endl; //会输出
}
对map和unordered_map,下标运算符提供了最简单的提取元素的方法,但它有一个副作用,当关键字还未在map中时,会插入一个具有给定关键字的新元素。但有时,我们只想知道一个给定的关键字是否在map中,而不想改变map,这种情况下应该使用find:
if (word_count.find("foobar") == word_count.end()) {
cout << "foobar is not in the map" << endl;
}
对于允许重复关键字的关联容器,这些重复关键字的元素会在容器中相邻存储。
若我们在作者和著作的映射中,想找到一个作者的所有著作:
方法一:
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会返回最后一个匹配的关键字元素之后的元素的迭代器。如果元素不在multimap中,lower_bond和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;
}
上例中,如果没有以search_item为关键字的元素,beg和end都将指向第一个关键字大于search_item的元素,有可能是尾后迭代器。
方法三:使用equal_range函数,此函数接受一个关键字,返回一个迭代器pair,若关键字存在,则第一个迭代器返回第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可以插入的安全位置:
for (auto pos = authors.equal_range(search_item); pos.first != pos.second; ++pos.first) {
cout << pos.first->second << endl;
}
multimap中的内容按序排列:
multimap<string, int> mmap = { { "c", 2 }, { "c", 2 }, { "d", 5 }, { "a", 1} };
for (pair<const string, int> &s : mmap) {
cout << s.first << endl; //输出accd
}
单词转换程序:读取两个文件,一个文件保存的是一些转换规则,另一个文件保存的是一个文本,单词转换文件内容:
输入文件内容:
程序代码:
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <map>
using namespace std;
map<string, string> BuildMap(ifstream& mapFile) {
map<string, string> trans_map;
string key, value;
while (mapFile >> key && getline(mapFile, value)) {
if (value.size() > 1) { //如果有值(键和值之间有空格,有值时value的size比1大)
trans_map[key] = value.substr(1); //去掉键和值之间的空格并保存到trans_map,隐含着忽略一个单词在转换文件中出现多次的情况
}
else {
throw runtime_error("no rule for " + key);
}
}
return trans_map;
}
string Transform(map<string, string> &trans_map, string &s) { //复杂类型用引用可以提高性能
map<string, string>::iterator it = trans_map.find(s);
if (it != trans_map.end()) {
return it->second;
}
else {
return s;
}
}
void Word_transform(ifstream& mapFile, ifstream& input) {
map<string, string> trans_map = BuildMap(mapFile);
string line;
while (getline(input, line)) {
istringstream sin(line);
string word;
bool isFirst = true;
while (sin >> word) {
if (isFirst) {
isFirst = false;
}
else {
cout << " "; //在每个单词前打印一个空格,但每行第一个单词就不必了
}
cout << Transform(trans_map, word);
}
cout << endl;
}
}
int main() {
ifstream map_file("文件路径"), input("文件路径");
Word_transform(map_file, input);
map_file.close();
input.close();
}
在以上代码中,将trans_map[key] = value.substr(1);
改为trans_map.insert({key, value.substr(1)});
效果为,在改之前,每次在map_file中找到关键字重复的条目时,会将关键字与最新(在文件中是最下面)的值关联,而修改后关键字会一直等于第一次插入时的值。
C++11定义了4个无序关联容器,这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数和关键字的==运算符。在关键字类型的元素没有明显的序关系的情况下,无序容器很有用,因为有时维护元素的序的代价非常高昂。
除了哈希管理外,无序容器还提供了与有序容器相同的操作,如find、insert等,并且无序容器也有允许重复值的版本,因此通常可以用无序容器替换对应的有序容器,反之亦然。但由于无序容器的元素未按顺序存储,一个使用无序容器的的程序的输出通常会与使用有序版本的不同。
无序容器在存储上组织为一组桶,每个桶保存0个或多个元素。无序容器使用一个哈希函数将元素映射到桶,为访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶,容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果元素允许重复关键字,所有具有相同关键字的元素也都会在一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
对于相同的参数,哈希函数必须总是产生相同的结果,理想情况下,哈希函数将每个特定值映射到唯一的桶,但将不同的元素映射到相同的桶也是允许的。当一个桶保存多个元素时,需要顺序搜索桶内元素。计算一个哈希值和桶中搜索通常都是很快的操作,但如果一个桶中保存了很多元素,那么查找速度会慢。
无序容器管理操作:
默认,无序容器使用关键字类型的==运算符来比较元素,无序容器还使用一个hash<key_type>来类型的对象来生成每个元素的哈希值。标准库为内置类型、一些标准库类型如string、智能指针提供了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();
}
我们的hasher函数的return后面定义了一个std::hash<std::string>临时变量(第一对圆括号),然后调用该临时变量函数operator()(第二对圆括号,这里是运算符重载),并传sd.isbn()的返回值作为参数。
类似地,eqOp函数通过比较ISBN号来比较两个Sales_data。
定义一个unordered_multiset:
using SD_multiset = unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>;
SD_multiset bookstore(42, hasher, eqOp); //参数是桶大小、哈希函数指针、相等性判断运算符指针
unordered_multiset<Sales_data, decltype(hasher)*> bookstore(42, hasher); //如果我们的类Sales_data定义了==运算符,就不需要相等性判断运算符指针了