《C++Primer(第5版)》第十一章笔记
Posted qq_34132502
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《C++Primer(第5版)》第十一章笔记相关的知识,希望对你有一定的参考价值。
第十一章——关联容器
关联容器支持高效的关键字查找和访问。
两个主要的关联容器(associative-container)类型是map
和set
。map中的元素是一些关键字值(key-value) 对:关键字起到索引的作用,值则表示与索引相关联的数据。set中每个元素只包含一个关键字:set支持高效的关键字查询操作——检查一个给定关键字是否在set中。例如,在某些文本处理过程中,可以用一个set来保存想要忽略的单词。字典则是一个很好的使用map的例子:可以将单词作为关键字,将单词释义作为值。
11.1 使用关联容器
map
是关键字值对的集合。例如,可以将一个人的名字作为关键字,将其电话号码作为值。我们称这样的数据结构为“将名字映射到电话号码”。map类型通常被称为关联数组(associative array)。关联数组与“正常”数组类似,不同之处在于其下标不必是整数。我们通过一个关键字而不是位置来查找值。给定一个名字到电话号码的map,我们可以使用一个人的名字作为下标来获取此人的电话号码。
与之相对,set就是关键字的简单集合。当只是想知道一个值是否存在时,set是最有用的。例如,一个企业可以定义一个名为bad_checks的set来保存那些曾经开过空头支票的人的名字。在接受一张支票之前, 可以查询bad_checks来检查顾客的名字是否在其中。
使用map
单词计数:
map<string, size_t> word_count;
string word;
while(cin >> word)
++word_count[word];
for(const auto &w : word_count)
cout << w.first << " occurs " << w.second
<< ((w.second > 1) ? " times" : " time") << endl;
while循环每次从标准输入读取一个单词。它使用每个单词对word_count进行下标操作。如果word还未在map中,下标运算符会创建一个新元素,其关键字为word,值为0。不管元素是否是新创建的,我们将其值加1。
使用set
上一个示例程序的一个合理扩展是:忽略常见单词,如"the"、 “and”、 "or”等。 我们可以使用set保存想忽略的单词,只对不在集合中的单词统计出现次数:
map<string, size_t> word_count;
set<string> exclude = {"Tghe", "But", "And", "Or"};
string word;
while (cin >> word)
if (exclude.find(word) == exclulde.end())
++word_count[word];
11.2 关联容器概述
当定义一个map
时,必须指明关键字类型和值类型;而定义set
只需指明关键字类型,因为set
没有值。
初始化multimap或multiset
下面的例子展示了具有唯一关键字的容器与允许重复关键字的容器之间的区别。首先,我们将创建一个名为ivec的保存int的vector,它包含20个元素:0到9每个整数有两个拷贝。我们将使用此vector初始化一个set和一个multiset:
vector<int> ivec;
for (auto i = 0; i != 10; ++i) {
ivec.push_back(i);
ivec.push_back(i);
}
set<int> iset(ivec.cbegin(), ivec.cend());
multiset<int> imulset(ivec.cbegin(), ivec.cend());
for_each(iset.cbegin(), iset.cend(), [](const int num) { cout << num << " "; });
cout << endl;
for_each(imulset.cbegin(), imulset.cend(), [](const int num) { cout << num << " "; });
cout << endl;
11.2.2 关键字类型的要求
关联容器对其关键字类型有一些限制。对于无序容器中关键字的要求,我们将在以后介绍。对于有序容器map、multimap、 set以及multiset,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的<运算符来比较两个关键字。在集合类型中,关键字类型就是元素类型;在映射类型中,关键字类型是元素的第一部分的类型。因此,word_count的关键字类型是string。类似的,exclude的关键字类型也是string。
有序容器的关键字类型
.可以向一个算法提供我们自己定义的比较操作,与之类似,也可以提供自己定义的操作来代替关键字上的<
运算符。所提供的操作必须在关键字类型上定义一个严格弱序。可以将严格弱序看作“小于等于”,虽然实际定义的操作可能是一个复杂的函数。无论我们怎样定义比较函数,它必须具备如下基本性质:
- 两个关键字不能同时“小于等于”对方:如果k1“小于等于”k2,那么k2绝不能“小于等于”k1。
- 如果k1“小于等于”k2,且k2“小于等于”k3,那么k1必须“小于等于”k3。
- 如果存在两个关键字,任何一个都不“小于等于”另一个,那么我们称这两个关键字是“等价”的。如果k1“等价于”k2,且k2“等价于”k3,那么k1必须“等价于”k3。
使用关键字类型的比较函数
例如,我们不能直接定义一个Sales_data的multiset,因为Sales_data没有<运算符。但是,可以用compareIsbn函数来定义一个multiset。此函数在sales_data对象的ISBN成员上定义了一个严格弱序。函数compareIsbn应该像下面这样定义:
bool compareIsbn(const Sales_data &lhs, const Sales_data &rhs){
return lhs.isbn() < rhs.isbn();
}
为了使用自己定义的操作,在定义multiset
时我们必须提供两个类型:关键字类型Sales_data,以及比较操作类型一应该是一 种函数指针类型,可以指向compareIsbn。当定义此容器类型的对象时,需要提供想要使用的操作的指针。在本例中,我们提供一个指向compareIsbn的指针:
// bookstore中多条记录可以有相同的ISBN
// bookstore中的元素以ISBN的顺序进行排列
multiset<Sales_data, decltype(compareIsbn)*> bookstore(compareIsbn);
此处,我们使用decltype
来指出自定义操作的类型。记住,当用decltype
来获得一个函数指针类型时,必须加上一个*
来指出我们要使用一个给定函数类型的指针。用compareIsbn
来初始化bookstore
对象,这表示当我们向bookstore添加元素时,通过调用compareIsbn来为这些元素排序。即,bookstore中的元素将按它们的ISBN成员的值排序。可以用compareIsbn
代替&compareIsbn
作为构造函数的参数,因为当我们使用一个函数的名字时,在需要的情况下它会自动转化为一个指针。当然,使用&compareIsbn的效果也是一样的。
11.2.3 pair类型
pair
标准库类型,它定义在头文件utility
中
与其他标准库类型不同,pair
的数据成员是public
的。两个成员分别命名为first
和second
。我们用普通的成员访问符号来访问它们,例如:
cout << w.first << " occurs " << w.second
<< ((w.second ? 1) ? " times" : " time") << endl;
w是指向map的某个引用,map的元素是pair
11.3 关联容器操作
对于set
类型,key_ype
和value_type
是一样的。set中保存的值就是关键字。
在一个map
中,元素是关键字-值对。即,每个元素是一个pair
对象,包含一个关键字和一个关联的值。由于我们不能改变一个元素的关键字,因此这些pair的关键字部分是const
的
只有map
类型(unordered_map
等)才定义了mapped_type
11.3.1 关联容器迭代器
当解引用一个关联容器迭代器时,我们会得到一个类型为容器的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的迭代器是const的
虽然set
类型同时定义了iterator
和const_iterator
类型,但两种类型都只允许只读访问set
中的元素。与不能改变一个map元素的关键字一样,一个set中的关键字也是const的。可以用一个set迭代器来读取元素的值,但不能修改
set<int>::iterator set_it = iset.begin();
if (set_it != iset.end()) {
*set_it = 42; // 错误:set的关键字是只读的
cout << *set_it << endl; // 正确
}
关联容器和算法
我们通常不对关联容器使用泛型算法。关键字是const这一特性意味着不能将关联容器传递给修改或重排容器元素的算法,因为这类算法需要向元素写入值,而set类型中的元素是const的,map中的元素是pair,其第一个成员是const的。
关联容器可用于只读取元素的算法。但是,很多这类算法都要搜索序列。由于关联容器中的元素不能通过它们的关键字进行(快速)查找,因此对其使用泛型搜索算法几乎总是个坏主意。例如,我们可以用泛型find算法来查找一个元素,但此算法会进行顺序搜索。使用关联容器定义的专用的find成员会比调用泛型find快得多。
在实际编程中,如果我们真要对一个关联容器使用算法,要么是将它当作一个源序列,要么当作一个目的位置。例如,可以用泛型copy算法将元素从一个关联容器拷贝到另一个序列。类似的,可以调用inserter将一个插入器绑定到一个关联容器。通过使用inserter,我们可以将关联容器当作一个目的位置来调用另一个算法。
11.3.2 添加元素
关联容器的insert
成员向容器中添加一个元素或一个元素范围。由于map
和set
(以及对应的无序类型)包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响:
insert有两个版本,分别接受一对迭代器 ,或是一个初始化器列表,这两个版本的行为类似对应的构造函数——对于一个给定的关键字,只有第一个带此关键字的元素才被插入到容器中。
向map添加元素
对一个map进行insert操作时,必须记住元素类型是pair
通常,对于想要插入的数据,并没有一个现成的pair
对象。可以在insert的参数列表中创建一个pair:
// 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));
构造一个恰当的pair
类型,并构造该类型的一个新对象,插入到map
中
检测insert的返回值
insert
(或emplace
)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair
,告诉我们插入操作是否成功。pair的first
成员是一个迭代器,指向具有给定关键字的元素;second
成员是一个bool
值,指出元素是插入成功还是已经存在于容器中。如果关键字已在容器中,则insert什么事情也不做,且返回值中的bool部分为false。如果关键字不存在,元素被插入容器中,且bool值为true。
例如:
map<string, size_t> word_count; // string到size_t的空map
string word;
while (cin >> word) {
auto ret = word_count.insert({word, 1});
if (!ret.second)
++ret.first->second;
}
展开递增语句
在这个版本的单词计数程序中,递增计数器的语句很难理解。通过添加一些括号来反映出运算符的优先级,会使表达式更容易理解一些:
++ret.first->second;
++((ret.first)->second); // 等价
向multiset或multimap添加元素
由于multi容器中的关键字不必唯一,在这些类型上调用inset总会插入一个元素:
multimap<string, string> authors;
// 插入第一个元素,关键字为Barth, John
authors.insert({"Barth, John", "Sot-Weed Factor"});
// 插入成功:插入第二个元素,关键字也为Barth, John
authors.insert({"Barth, John", "Lost in the Funhouse"});
11.3.3 删除元素
关联容器定义了三个版本的erase
。与顺序容器一样,我们可以通过传递给erase一个迭代器或一个迭代器对来删除一个元素或者一个元素范围。这两个版本的erase与对应的顺序容器的操作非常相似:指定的元素被删除,函数返回void。
关联容器提供一个额外的erase操作,它接受一个key_type
参数。此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除的元素的数量。我们可以用此版本在打印结果之前从word_count中删除一个特定的单词:
if (word_count.erase(removal_word))
cout << "ok: " << removal_word << " removed\\n";
else cout << "oops: " << removal_word << " not found!\\n";
对于保存不重复关键字的容器,erase的返回值总是0或1。对允许重复关键字的容器,删除元素的数量可能大于1。
11.3.4 map的下标操作
map
和unordered_map
容器提供了下标运算符和一个对应的at
函数。set
类型不支持下标,因为set中没有与关键字相关联的“值”。元素本身就是关键字,因此“获取与一个关键字相关联的值”的操作就没有意义了。我们不能对一个multimap
或一个unordered_multimap
进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。
类似我们用过的其他下标运算符,map下标运算符接受一个索引(即,一个关键字),获取与此关键字相关联的值。但是,与其他下标运算符不同的是,如果关键字并不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化。例如:
map<string, size_t> word_count;
// 插入一个关键字为Anna的元素,管理安置进行值初始化;然后将1赋予它
word_count["Anna"] = 1;
将会执行如下操作:
- 在word_count中搜索关键字Anna的元素,未找到
- 将一个新的关键字-值对插入到word_count中。关键字是一个const string,保存Anna。值进行初始化,在本例中意味着0
- 提取出新插入的元素,并将值1赋予它
由于下标运算符可能插入一个新元素,我们只可以对非const的map使用下标操作。
使用下标操作的返回值
map的下标运算符与我们用过的其他下标运算符的另一个不同之处是其返回类型。通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。 但对map则不然:当对一个map进行下标操作时,会获得一个mapped_type
对象:但当解引用一个map迭代器时,会得到一个value_type
对象。
与其他下标运算符相同的是,map的下标运算符返回一个左值。由于返回的是一个左值,所以我们既可以读也可以写元素:
cout << word_count["Anna"];
++word_count["Anna"];
cout << word_count["Anna"];
如果关键字还未在map中,下标运算符会添加一个新元素,这特性允许我们编写出异常简洁的程序。另一方面,有时只是想知道一个元素是否已在map中,但在不存在时并不想添加元素。在这种情况下,就不能使用下标运算符。
11.3.5 访问元素
关联容器提供多种查找一个指定元素的方法。应该使用哪个操作依赖于我们要解决什么问题。如果我们所关心的只不过是一个特定元素是否已在容器中,可能find
是最佳选择。对于不允许重复关键字的容器,可能使用find还是count
没什么区别。但对于允许重复关键字的容器,count还会做更多的工作:如果元素在容器中,它还会统计有多少个元素有相同的关键字。如果不需要计数,最好使用find。
对map使用find代替下标操作
有时,我们只是想知道一个给定关键字是否在map中,而不想改变map。这样就不能使用下标运算符来检查一个元素是否存在,因为如果关键字不存在的话,下标运算符会插入一个新元素。在这种情况下,应该使用find
在multimap或multiset中查找元素
在一个不允许重复关键字的关联容器中查找一个元素是一件很简单的事情一元素要么在容器中,要么不在。但对于允许重复关键字的容器来说,过程就更为复杂:在容器中可能有很多元素具有给定的关键字。如果一个multimap
或multiset
中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。
例如,给定一个从作者到著作题目的映射,我们可能想打印一个特定作者的所有著作。可以用三种不同方法来解决这个问题。最直观的方法是使用find和count:
string earch_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
返回的迭代器则指向最后一个匹配给定关键字的元素之后的位置。如果元素不在multimap中,则lower_bound和upper_ bound会返回相等的迭代器——指向一个不影响排序的关键字插入位置。
因此,用相同的关键字调用lower_bound和upper_bound会得到一个迭代器范围,表示所有具有该关键字的元素的范围。
当然,这两个操作返回的迭代器可能是容器的尾后迭代器。如果我们查找的元素具有容器中最大的关键字,则此关键字的upper_bound返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则lower_bound 返回的也是尾后迭代器。
重写之前的程序:
// authors和search_item的定义,与前面的程序一样
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.3.6 一个单词转换的map
我们将以一个程序结束本节的内容,它将展示map的创建、搜索以及遍历。这个程序的功能是这样的:给定一个string,将它转换为另一个string。程序的输入是两个文件。第一个文件保存的是一些规则,用来转换第二个文件中的文本。每条规则由两部分组成:一个可能出现在输入文件中的单词和一个用来替换它的短语。表达的含义是,每当第一个单词出现在输入中时,我们就将它替换为对应的短语。第二个输入文件包含要转换的文本。.
单词转换文件的内容如下所示:
brb be right bck
k okay?
y why
r are
u you
pic picture
thk thanks!
18r later
我们希望转换的文本为:
where r u
u dont u send me a pic
k thk 18r
则程序应该生成这样的输出:
where are you
why dont you send me a picture
okay? thanks! later
单词转换程序
我们的程序将使用三个函数。函数word_transform
管理整个过程。它接受两个ifstream
参数:第一个参数应绑定到单词转换文件,第二个参数应绑定到我们要转换的文本文件。函数buildMap
会读取转换规则文件,并创建一个map
,用于保存每个单词到其转换内容的映射。函数transform
接受一个string
,如果存在转换规则,返回转换后的内容。
首先定义word_transform
函数,最重要的部分是调用buildMap
和transform
:
void word_transform(ifstream &map_file, ifstream &input) {
auto trans_map = buildMap(map_file); // 保存转换规则
string text; // 保存输入中的每一行
while (getline(input, test)) { // 读取一行输入
istringstream stream(text); // 读取每个单词
string word;
bool firstword = true; // 控制是否打印空格
while (stream >> word) {
if (firstword)
firstword = false;
else
cout << " "; // 在单词间打印一个空格
// transform返回它的第一个参数或转换之后的形式
cout << transform(word, trans_map);
}
cout << endl;
}
}
建立转换映射
函数buildMap
读入给定文件,建立转换映射:
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;
}
map_file中的每一行对应一条规则。每条规则由一个单词和一个短语组成,短语可能包含多个单词。我们用>>
读取要转换的单词,存入key中,并调用getline
读取这一行中的剩余内容存入value。由于getline不会跳过前导空格,需要我们来跳过单词和它的转换内容之间的空格。在保存转换规则之前,检查是否获得了一个以上的字符。如果是,调用substr
来跳过分隔单词及其转换短语之间的前导空格,并将得到的子字符串存入trans_map
生成转换文本
函数transform
进行实际的转换工作。其参数是需要转换的string的引用和转换规则map。如果给定string在map中,transform返回相应的短语。否则,transform直接返回原string:
const string& transform(const string &s, const map<string, string> &m){
// 实际的转换工作;此部分是核心
auto map_it = m.find(s);
// 如果单词在转换规则map中
if (map_it != m.cend())
return map_it->second;
else
return s;
}
11.4 无序容器
新标准定义了4个无序关联容器(unordered associative container)。这些容器不是使用比较运算符来组织元素,而是使用一个哈希函数(hash function) 和关键字类型的==
运算符。在关键字类型的元素没有明显的序关系的情况下,无序容器是非常有用的。在某些应用中,维护元素的序代价非常高昂,此时无序容器也很有用。
虽然理论上哈希技术能获得更好的平均性能,但在实际中想要达到很好的效果还需要进行一些性能测试和调优工作。因此,使用无序容器通常更为简单(通常也会有更好的性能)。
使用无序容器
除了哈希管理操作之外,无序容器还提供了与有序容器相同的操作(find
、insert
等)。这意味着我们曾用于map和set的操作也能用于unordered_map
和unordered_set
。类似的,无序容器也有允许重复关键字的版本。
例如,使用unordered_map
重写最初的单词计数程序:
unordered_map<string, size_t> word_count;
string word;
while(cin >> word)
++word_count[word];
for (const auto &w : word_count)
cout << w.first << " occurs " << w.second
<< ((w.second > 1) ? " times" : " time") << endl;
管理桶
无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。如果容器允许重复关键字,所有具有相同关键字的元素也都会在同一个桶中。因此,无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
对于相同的参数,哈希函数必须总是产生相同的结果。理想情况下,哈希函数还能将每个特定的值映射到唯一的桶。但是,将不同关键字的元素映射到相同的桶也是允许的。当一个桶保存多个元素时,需要顺序搜索这些元素来查找我们想要的那个。计算一个元素的哈希值和在桶中搜索通常都是很快的操作。但是,如果一个桶中保存了很多元素,那么查找一个特定元素就需要大量比较操作。
无序容器提供了一组管理桶的函数。这些成员函数允许我们查询容器的状态以及在必要时强制容器进行重组。
无序容器对关键字类型的要求
默认情况下,无序容器使用关键字类型的==
运算符来比较元素,它们还使用一个hash<key_type>
类型的对象来生成每个元素的哈希值。标准库为内置类型(包括指针)提供了hash
模板。还为一些标准库类型,包括string和智能指针类型定义了hash。因此,我们可以直接定义关键字是内置类型(包括指针类型)string
还是智能指针类型的无序容器。
但是,我们不能直接定义关键字类型为自定义类类型的无序容器。与容器不同,不能直接使用哈希模板,而必须提供我们自己的hash模板版本。
我们不使用默认的hash,而是使用另一种方法, 类似于为有序容器重载关键字类型的默认比较操作。为了能将Sale_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
函数使用一个标准库hash类型对象来计算ISBN成员的哈希值,该hash类型建立在string类型之上。类似的,