《C++ Primer》习题参考答案:第11章 - 关联容器

专栏C++学习笔记

《C++ Primer》学习笔记/习题答案 总目录

——————————————————————————————————————————————————————

📚💻 Cpp-Prime5 + Cpp-Primer-Plus6 源代码和课后题

第11章 - 关联容器

练习11.1

描述 mapvector 的不同。

解:

学习关联容器,理解与顺序容器的不同,最关键的是理解其基础的数据结构,随后它所表现出来的一些性质就很自然能够理解了。

两类容器的根本差别在于,顺序容器中的元素是“顺序”存储的(链表容器中的元素虽然不是在内存中“连续”存储的,但仍然是按“顺序”存储的)。理解“顺序”的关键,是理解容器支持的操作形式以及效率。

对于 vector 这样的顺序容器,元素在其中按顺序存储,每个元素有唯一对应的位置编号,所有操作都是按编号(位置)进行的。例如,获取元素(头、尾、用下标获取任意位置)、插入删除元素(头、尾、任意位置)、遍历元素(按元素位置顺序逐一访问)。底层的数据结构是数组、链表,简单但已能保证上述操作的高效。而对于依赖值的元素访问,例如查找(搜索)给定值(find),在这种数据结构上的实现是要通过遍历完成的,效率不佳。

map 这种关联容器,就是为了高效实现“按值访问元素”这类操作而设计的。为了达到这一目的,容器中的元素是按关键字值存储的,关键字值与元素数据建立起对应关系,这就是“关联”的含义。底层数据结构是红黑树、哈希表等,可高效实现按关键字值查找、添加、删除元素等操作。

练习11.2

分别给出最适合使用 listvectordequemap 以及 set 的例子。

解:

  • 若元素很小(例如 int),大致数量预先可知,在程序运行过程中不会剧烈变化,
    • 大部分情况下只在末尾添加或删除需要频繁访问任意位置的元素,则 vector 可带来最高的效率。
    • 若需要频繁在头部和尾部添加或删除元素,则 deque 是最好的选择。
  • 如果元素较大(如大的类对象),数量预先不知道,或是程序运行过程中频繁变化,对元素的访问更多是顺序访问全部或很多元素,则 list 很适合。
  • map 很适合对一些对象按它们的某个特征进行访问的情形。典型的例如按学生的名字来查询学生信息,即可将学生名字作为关键字,将学生信息作为元素值,保存在 map 中。
  • set,顾名思义,就是集合类型。当需要保存特定的值集合——通常是满足/不满足某种要求的值集合,用 set 最为方便。

练习11.3

编写你自己的单词计数程序。

解:

#include <iostream>
#include <string>
#include <algorithm>
#include <map>
#include <fstream>          // file I/O support
#include <cstdlib>          // support for exit()
using namespace std;
int main()
{
	char filename[20];
	cout << "Enter name of data file: ";
	cin.getline(filename, 20);
	ifstream inFile;        // object for handling file input
	inFile.open(filename);  // associate inFile with a file
	if (!inFile.is_open())  // failed to open file
	{
		cout << "Could not open the file " << filename << endl;
		cout << "Program terminating.\n";
		// cin.get();    	// keep window open
		exit(EXIT_FAILURE);
	}
	map<string, size_t> word_count;
	string word;
	while (inFile >> word)
	{
		++word_count[word];
	}
	for (const auto &w : word_count){
		cout << w.first << "出现了" << w.second << "次" << endl;
	}
	inFile.close();         // finished with the file

	system("pause");
	return 0;
}

在这里插入图片描述
在这里插入图片描述

练习11.4

扩展你的程序,忽略大小写和标点。例如,“example.”、“example,” 和 “Example” 应该递增相同的计数器。

解:

编写函数 trans,将单词中的标点去掉,将大写都转换为小写。

具体方法是:

遍历字符串,对每个字符首先检查是否是大写(ASCII 值在 AZ 之间),若是,将其转换为小写(减去 A,加上 a);否则,检查它是否带标点,若是,将其删除。最终,将转换好的字符串返回。

#include <iostream>
#include <string>
#include <algorithm>
#include <map>
#include <fstream>          // file I/O support
#include <cstdlib>          // support for exit()
using namespace std;
string &trans(string &s){
	for (int p = 0; p < s.size(); p++){
		if (s[p] >= 'A' && s[p] <= 'Z')
			s[p] -= ('A' - 'a');
		else if (s[p] == ',' || s[p] == '.')
			s.erase(p, 1);
	}
	return s;
}
int main()
{
	char filename[20];
	cout << "Enter name of data file: ";
	cin.getline(filename, 20);
	ifstream inFile;        // object for handling file input
	inFile.open(filename);  // associate inFile with a file
	if (!inFile.is_open())  // failed to open file
	{
		cout << "Could not open the file " << filename << endl;
		cout << "Program terminating.\n";
		// cin.get();    	// keep window open
		exit(EXIT_FAILURE);
	}
	map<string, size_t> word_count;
	string word;
	while (inFile >> word)
	{
		++word_count[trans(word)];
	}
	for (const auto &w : word_count){
		cout << w.first << "出现了" << w.second << "次" << endl;
	}
	inFile.close();         // finished with the file

	system("pause");
	return 0;
}

在这里插入图片描述

练习11.5

解释 mapset 的区别。你如何选择使用哪个?

解:

  • 当需要查找给定值所对应的数据时,应使用 map,其中保存的是 < 关键字,值对,按关键字访问值。

  • 如果只需判定给定值是否存在时,应使用 set,它是简单的值的集合。

练习11.6

解释 setlist 的区别。你如何选择使用哪个?

解:

两者都可以保存元素集合。

  • 如果只需要顺序访问这些元素,或是按位置访问元素,那么应使用 list
  • 如果需要快速判定是否有元素等于给定值,则应使用 set

练习11.7

定义一个 map,关键字是家庭的姓,值是一个 vector,保存家中孩子(们)的名。编写代码,实现添加新的家庭以及向已有家庭中添加新的孩子。

解:

map 的关键字类型是 string,值类型是 vector<string>

我们定义函数 add family 添加一个家庭,注意,必须先检查是否已有这个家庭,若不做这个检查,则可能将已有家庭的孩子名字清空(如 main 函数中的王姓家庭的添加顺序)。若确实还没有这个家庭,则创建一个空的 vector<string>,表示这个家庭的孩子名字列表。

函数 add_child 向一个已有家庭添加孩子的名字:首先用 [] 运算符取出该家庭的 vector,然后调用 push back 将名字追加到 vector 末尾。

#include <iostream>
#include <string>
#include <algorithm>
#include <map>
#include <vector>
using namespace std;
void add_family(map<string, vector<string>> &families, 
	const string &family){
	if (families.find(family) == families.end())
		families[family] = vector<string>();
}
void add_child(map<string, vector<string>> &families, 
	const string &family, const string &child){
	families[family].push_back(child);
}
int main(){
	map<string, vector<string>>families;
	add_family(families, "张");
	add_child(families, "张", "强");
	add_child(families, "张", "刚");
	add_child(families, "王", "五");
	add_family(families, "王");
	for (auto f : families){
		cout << f.first << "家的孩子:";
		for (auto c : f.second)
			cout << c << " ";
		cout << endl;
	}
	system("pause");
	return 0;
}

在这里插入图片描述

练习11.8

编写一个程序,在一个 vector 而不是一个 set 中保存不重复的单词。使用 set 的优点是什么?

解:

使用 vector 保存不重复单词,需要用 find 查找新读入的单词是否已在 vector 中,若不在(返回尾后迭代器),才将单词加入 vector。而使用 set,检查是否重复的工作是由 set 模板负责的,程序员无须编写对应代码,程序简洁很多。

更深层次的差别,vector 是无序线性表,find 查找指定值只能采用顺序查找方式,所花费的时间与 vector.size() 呈线性关系。而 set 是用红黑树实现的,花费的时间与 vector.size() 呈对数关系。当单词数量已经非常多时,set 的性能优势是巨大的。当然,vector 也不是毫无用处。它可以保持单词的输入顺序,而 set 则不能,遍历 set,元素是按值的升序被遍历的。
在这里插入图片描述
vector 版本:

#include <iostream>
#include <string>
#include <algorithm>
#include <vector>
#include <fstream>          // file I/O support
#include <cstdlib>          // support for exit()
using namespace std;
string &trans(string &s){
	for (int p = 0; p < s.size(); p++){
		if (s[p] >= 'A' && s[p] <= 'Z')
			s[p] -= ('A' - 'a');
		else if (s[p] == ',' || s[p] == '.')
			s.erase(p, 1);
	}
	return s;
}
int main()
{
	char filename[20];
	cout << "Enter name of data file: ";
	cin.getline(filename, 20);
	ifstream inFile;        // object for handling file input
	inFile.open(filename);  // associate inFile with a file
	if (!inFile.is_open())  // failed to open file
	{
		cout << "Could not open the file " << filename << endl;
		cout << "Program terminating.\n";
		// cin.get();    	// keep window open
		exit(EXIT_FAILURE);
	}
	vector<string> unique_word;
	string word;
	while (inFile >> word)
	{
		trans(word);
		if (find(unique_word.begin(), unique_word.end(), word) == unique_word.end())
			unique_word.push_back(word);
	}
	for (const auto &w : unique_word){
		cout << w << endl;
	}
	cout << endl;
	inFile.close();         // finished with the file

	system("pause");
	return 0;
}

在这里插入图片描述
set 版本:

#include <iostream>
#include <string>
#include <algorithm>
#include <set>
#include <fstream>          // file I/O support
#include <cstdlib>          // support for exit()
using namespace std;
string &trans(string &s){
	for (int p = 0; p < s.size(); p++){
		if (s[p] >= 'A' && s[p] <= 'Z')
			s[p] -= ('A' - 'a');
		else if (s[p] == ',' || s[p] == '.')
			s.erase(p, 1);
	}
	return s;
}
int main()
{
	char filename[20];
	cout << "Enter name of data file: ";
	cin.getline(filename, 20);
	ifstream inFile;        // object for handling file input
	inFile.open(filename);  // associate inFile with a file
	if (!inFile.is_open())  // failed to open file
	{
		cout << "Could not open the file " << filename << endl;
		cout << "Program terminating.\n";
		// cin.get();    	// keep window open
		exit(EXIT_FAILURE);
	}
	set<string> unique_word;
	string word;
	while (inFile >> word)
	{
		trans(word);
		if (find(unique_word.begin(), unique_word.end(), word) == unique_word.end())
			unique_word.insert(word);
	}
	for (const auto &w : unique_word){
		cout << w << endl;
	}
	cout << endl;
	inFile.close();         // finished with the file

	system("pause");
	return 0;
}

在这里插入图片描述

练习11.9

定义一个 map,将单词与一个行号的 list 关联,list 中保存的是单词所出现的行号。

解:

#include <iostream>
#include <string>
#include <algorithm>
#include <set>
#include <map>
#include <list>
#include <sstream>
#include <fstream>          // file I/O support
#include <cstdlib>          // support for exit()
using namespace std;
string &trans(string &s){
	for (int p = 0; p < s.size(); p++){
		if (s[p] >= 'A' && s[p] <= 'Z')
			s[p] -= ('A' - 'a');
		else if (s[p] == ',' || s[p] == '.')
			s.erase(p, 1);
	}
	return s;
}
int main()
{
	char filename[20];
	cout << "Enter name of data file: ";
	cin.getline(filename, 20);
	ifstream inFile;        // object for handling file input
	inFile.open(filename);  // associate inFile with a file
	if (!inFile.is_open())  // failed to open file
	{
		cout << "Could not open the file " << filename << endl;
		cout << "Program terminating.\n";
		// cin.get();    	// keep window open
		exit(EXIT_FAILURE);
	}
	map<string, list<int>> word_lineno;
	string line;
	string word;
	int lineno = 0;
	while (getline(inFile,line))
	{
		lineno++;
		istringstream l_in(line);
		while (l_in >> word)
		{
			trans(word);
			word_lineno[word].push_back(lineno);
		}
	}
	for (const auto &w : word_lineno){
		cout << w.first << "所在行";
		for (const auto &i : w.second)
			cout << i << " ";
		cout << endl;
	}
	inFile.close();         // finished with the file

	system("pause");
	return 0;
}

在这里插入图片描述
在这里插入图片描述

练习11.10

可以定义一个 vector<int>::iteratorintmap 吗?list<int>::iteratorintmap 呢?对于两种情况,如果不能,解释为什么。

解:

由于有序容器要求关键字类型必须支持比较操作 <

  • 因此 map<vector<int>::iterator, int>ml; 是可以的,因为 vector 的迭代器支持比较操作。
  • map<list<int>::iterator, int>m2; 是不行的,因为 list 的元素不是连续存储,其迭代器不支持比较操作。

练习11.11

不使用 decltype 重新定义 bookstore

解:

首先用 typedef 定义与 compareIsbn 相容的函数指针类型,然后用此类型声明 multiset 即可。

typedef bool (*pf)(const Sales_data &, const Sales_data &);
multiset<Sales_data, pf> bookstore(compareIsbn);

练习11.12

编写程序,读入 stringint 的序列,将每个 stringint 存入一个 pair 中,pair 保存在一个 vector 中。

解:

#include <iostream>
#include <string>
#include <algorithm>
#include <utility>
#include <vector>
#include <fstream>          // file I/O support
#include <cstdlib>          // support for exit()
using namespace std;
int main()
{
	char filename[20];
	cout << "Enter name of data file: ";
	cin.getline(filename, 20);
	ifstream inFile;        // object for handling file input
	inFile.open(filename);  // associate inFile with a file
	if (!inFile.is_open())  // failed to open file
	{
		cout << "Could not open the file " << filename << endl;
		cout << "Program terminating.\n";
		// cin.get();    	// keep window open
		exit(EXIT_FAILURE);
	}
	vector<pair<string, int>> data;
	string s;
	int v;
	while (inFile >> s && inFile >> v)
	{
		data.push_back(pair<string, int>(s, v));
	}
	for (const auto &d : data){
		cout << d.first << " " << d.second << endl;
	}
	inFile.close();         // finished with the file

	system("pause");
	return 0;
}

在这里插入图片描述

练习11.13

在上一题的程序中,至少有三种创建 pair 的方法。编写此程序的三个版本,分别采用不同的方法创建 pair。解释你认为哪种形式最易于编写和理解,为什么?

解:

vec.push_back(make_pair(str, i));
vec.push_back({ str, i });
vec.push_back(pair<string, int>(str, i)); 

使用花括号的初始化器最易于理解和编写。

练习11.14

扩展你在11.2.1节练习中编写的孩子姓达到名的 map,添加一个 pairvector,保存孩子的名和生日。

解:

在本题中,我们将家庭的姓映射到孩子信息的列表,而不是简单的孩子名字的列表。因此,将在 vector 中的元素类型声明为 pair<string, string>,两个 string分别表示孩子的名字和生日。在添加孩子信息时,用列表初始化创建名字和生日的 pair,添加到 vector 中即可。

#include <iostream>
#include <string>
#include <algorithm>
#include <map>
#include <vector>
using namespace std;
void add_family(map<string, vector<pair<string, string>>> &families,
	const string &family){
	families[family];
}
void add_child(map<string, vector<pair<string, string>>> &families,
	const string &family, const string &child, const string &birthday){
	families[family].push_back({ child, birthday });
}
int main(){
	map<string, vector<pair<string, string>>> families;
	add_family(families, "张");
	add_child(families, "张", "强", "1970-1-1");
	add_child(families, "张", "刚", "1980-1-1");
	add_child(families, "王", "五", "1990-1-1");
	add_family(families, "王");
	for (auto f : families){
		cout << f.first << "家的孩子:";
		for (auto c : f.second)
			cout << c.first << "(生日" << c.second << "),";
		cout << endl;
	}
	system("pause");
	return 0;
}

在这里插入图片描述

练习11.15

对一个 intvector<int>map,其 mapped_typekey_typevalue_type 分别是什么?

解:

  • mapped_typevector<int>
  • key_typeint
  • value_typepair<const int, vector<int>>

练习11.16

使用一个 map 迭代器编写一个表达式,将一个值赋予一个元素。

解:

解引用关联容器的迭代器,得到的是 value_type 的值的引用。因此对 map 而言,得到的是一个 pair 类型的引用,其 first 成员保存 const 的关键字,second 成员保存值。因此,通过迭代器只能修改值,而不能改变关键字。

map<int, int>m;
auto it=m.begin();
it->second = 0;

练习11.17

假定 c 是一个 stringmultisetv 是一个 stringvector,解释下面的调用。指出每个调用是否合法:

copy(v.begin(), v.end(), inserter(c, c.end()));
copy(v.begin(), v.end(), back_inserter(c));
copy(c.begin(), c.end(), inserter(v, v.end()));
copy(c.begin(), c.end(), back_inserter(v));

解:

set 的迭代器是 const 的,因此只允许访问 set 中的元素,而不能改变 set。与 map 一样,set 的关键字也是 const,因此也不能通过迭代器来改变 set 中元素的值。

因此,前两个调用试图将 vector 中的元素复制到 set 中,是非法的。

而后两个调用将 set 中的元素复制到 vector 中,是合法的。

练习11.18

写出第382页循环中 map_it 的类型,不要使用 autodecltype

解:

在这里插入图片描述

pair<const string, size_t>::iterator

练习11.19

定义一个变量,通过对11.2.2节中的名为 bookstoremultiset 调用 begin() 来初始化这个变量。写出变量的类型,不要使用 autodecltype

解:

typedef bool (*pf)(const Sales_data &, const Sales_data &); 
multiset<Sales_data, pf> bookstore(compareIsbn); 
......
pair<const Sales_data, pf>::iterator it = bookstore.begin();

练习11.20

重写11.1节练习的单词计数程序,使用 insert 代替下标操作。你认为哪个程序更容易编写和阅读?解释原因。

解:

  • 使用 insert 操作的方式是:构造一个 pair(单词,1),用 insert 将其插入容器,返回一个 pair。若单词已存在,则返回 pairsecond 成员为 false,表示插入失败,程序员还需通过返回 pairfirst 成员(迭代器)递增已有单词的计数器。判断单词是否已存在,并进行相应操作的工作完全是由程序员负责的。

  • 使用下标操作的方式是:以单词作为下标获取元素值,若单词已存在,则提取出已有元素的值;否则,下标操作将 pair(单词,0)插入容器,提取出新元素的值。单词是否已存在的相关处理完全是由下标操作处理的,程序员不必关心,直接访问提取出的值就行了。

显然,对于单词计数问题来说,下标操作更简洁易读。

#include <iostream>
#include <string>
#include <algorithm>
#include <map>
#include <fstream>          // file I/O support
#include <cstdlib>          // support for exit()
using namespace std;
int main()
{
	char filename[20];
	cout << "Enter name of data file: ";
	cin.getline(filename, 20);
	ifstream inFile;        // object for handling file input
	inFile.open(filename);  // associate inFile with a file
	if (!inFile.is_open())  // failed to open file
	{
		cout << "Could not open the file " << filename << endl;
		cout << "Program terminating.\n";
		// cin.get();    	// keep window open
		exit(EXIT_FAILURE);
	}
	map<string, size_t> word_count;
	string word;
	while (inFile >> word)
	{
		auto ret = word_count.insert({ word, 1 });
		if (!ret.second)
			++ret.first->second;
	}
	for (const auto &w : word_count){
		cout << w.first << "出现了" << w.second << "次" << endl;
	}
	inFile.close();         // finished with the file

	system("pause");
	return 0;
}

在这里插入图片描述

练习11.21

假定 word_count 是一个 stringsize_tmapword 是一个 string,解释下面循环的作用:

while (cin >> word)
	++word_count.insert({word, 0}).first->second;

解:

循环不断从标准输入读入单词(字符串),直至遇到文件结束或错误。

每读入一个单词,构造 pair{word, 0},通过 insert 操作插入到 word_count 中。insert 返回一个 pair,其 first 成员是一个迭代器。若单词(关键字)已存在于容器中,它指向已有元素;否则,它指向新插入的元素。

因此,.first 会得到这个迭代器,指向 word 对应的元素。继续使用 ->second,可获得元素的值的引用,即单词的计数。若单词是新的,则其值为0,若已存在,则值为之前出现的次数。对其进行递增操作,即完成将出现次数加1。

用这种方法,上一题可稍微简单些。

练习11.22

给定一个 map<string, vector<int>>,对此容器的插入一个元素的 insert 版本,写出其参数类型和返回类型。

解:

参数类型是一个 pairfirst 成员的类型是 map 的关键字类型 stringsecond 成员的类型是 map 的值类型 vector<int>

pair<string, vector<int>>

返回类型也是一个 pairfirst 成员的类型是 map 的迭代器,second 成员的类型是布尔型:

pair<map<string, vector<int>>::iterator, bool>

练习11.23

11.2.1节练习中的 map 以孩子的姓为关键字,保存他们的名的 vector,用 multimap 重写此 map

解:

#include <iostream>
#include <string>
#include <algorithm>
#include <map>
using namespace std;
void add_child(multimap<string, string> &families,
	const string &family, const string &child){
	families.insert({ family, child });
}
int main(){
	multimap<string, string> families;
	add_child(families, "张", "强");
	add_child(families, "张", "刚");
	add_child(families, "王", "五");
	for (auto f : families){
		cout << f.first << "家的孩子:" << f.second << endl;
	}
	system("pause");
	return 0;
}

在这里插入图片描述

练习11.24

下面的程序完成什么功能?

map<int, int> m;
m[0] = 1;

解:

m 中已有关键字0,下标操作提取出其值,赋值语句将值置为1。否则,下标操作会创建一个 pair(0, 0),即关键字为0,值为0(值初始化),将其插入到 m 中,然后提取其值,赋值语句将值置为1。

练习11.25

对比下面的程序与上一题程序

vector<int> v;
v[0] = 1;

解:

对于 m,"0”表示“关键字0”。而对于 v,“0”表示“位置0”。

v 中已有不少于一个元素,即,存在“位置0”元素,则下标操作提取出此位置的元素(左值),赋值操作将其置为1。而 map 的元素是 pair 类型,下标提取的不是元素,而是元素的第二个成员,即元素的值。

v 尚为空,则下标提取出的是一个非法左值(下标操作不做范围检查),向其赋值可能导致系统崩溃等严重后果。

练习11.26

可以用什么类型来对一个 map 进行下标操作?下标运算符返回的类型是什么?请给出一个具体例子——即,定义一个 map,然后写出一个可以用来对 map 进行下标操作的类型以及下标运算符将会返会的类型。

解:

map 进行下标操作,应使用其 key_type,即关键字的类型。

而下标操作返回的类型是 mapped_type,即关键字关联的值的类型。

示例如下:

  • map 类型:map<string,int>
    • 用来进行下标操作的类型:string
    • 下标操作返回的类型:int

练习11.27

对于什么问题你会使用 count 来解决?什么时候你又会选择 find 呢?

解:

find 查找关键字在容器中出现的位置,而 count 则还会统计关键字出现的次数。因此,当我们希望知道(允许重复关键字的)容器中有多少元素的关键字与给定关键字相同时,使用 count

当我们只关心关键字是否在容器中时,使用 find 就足够了。特别是,对于不允许重复关键字的关联容器,findcount 的效果没有什么区别,使用 find 就可以了。或者,当我们需要获取具有给定关键字的元素(而不只是统计个数)时,也需要使用 find

find 和下标操作有一个重要区别,当给定关键字不在容器中时,下标操作会插入一个具有该关键字的元素。因此,当我们想检查给定关键字是否存在时,应该用 find 而不是下标操作。

练习11.28

对一个 stringintvectormap,定义并初始化一个变量来保存在其上调用 find 所返回的结果。

解:

find 返回一个迭代器,指向具有给定关键字的元素(若不存在则返回尾后迭代器),因此其返回类型是容器的迭代器。

// map类型
map<string, vector<int>> m;
// 保存find返回结果的变量
map<string, vector<int>>::iterator iter;

练习11.29

如果给定的关键字不在容器中,upper_boundlower_boundequal_range 分别会返回什么?

解:

lower_bound 返回第一个具有给定关键字的元素,upper_bound则返回最后一个具有给定关键字的元素之后的位置。即,这两个迭代器构成包含所有具有给定关键字的元素的范围。若给定关键字不在容器中,两个操作显然应构成一个空范围,它们返回相当的迭代器,指出关键字的正确插入位置——不影响关键字的排序。如果给定关键字比容器中所有关键字都大,则此位置是容器的尾后位置 end

equal_range 返回一个 pair,其 first 成员等价于 lower_bound 返回的迭代器,second 成员等价于 upper_bound 返回的迭代器。因此,若给定关键字不在容器中,firstsecond 都指向关键字的正确插入位置,两个迭代器构成一个空范围。

练习11.30

对于本节最后一个程序中的输出表达式,解释运算对象 pos.first->second 的含义。

解:

equal_range 返回一个 pair,其 first 成员与 lower bound 的返回结果相同,即指向容器中第一个具有给定关键字的元素。因此,对其解引用会得到一个 value_type 对象,即一个 pair,其 first 为元素的关键字,即给定关键字,而 second 为关键字关联的值。在本例中,关键字为作者,关联的值为著作的题目。因此 pos.first->second 即获得给定作者的第一部著作的题目。

练习11.31

编写程序,定义一个作者及其作品的 multimap。使用 findmultimap 中查找一个元素并用 erase 删除它。确保你的程序在元素不在 map 中时也能正常运行。

解:

将数据插入 multimap,需使用 insert 操作。

multimap 中查找具有给定关键字的元素,有几种方法:

  • 使用 find 只能查找第一个具有给定关键字的元素,要找到所有具有给定关键字的元素,需编写循环;
  • lower_boundupper_bound 配合使用,可找到具有给定关键字的元素的范围;
  • equal_range 最为简单,一次即可获得要查找的元素范围。将找到的范围传递给 erase,即可删除指定作者的所有著作。

为了解决元素不在 multimap 中的情况,首先检查 equal_range 返回的两个迭代器,若相等(空范围),则什么也不做。范围不为空时,才将迭代器传递给 erase,删除所有元素。

#include <iostream>
#include <string>
#include <algorithm>
#include <map>
using namespace std;
void remove_author(multimap<string, string> &books,
	const string &author){
	auto pos = books.equal_range(author);
	if (pos.first == pos.second)
		cout << "没有" << author << "这个作者" << endl << endl;
	else
		books.erase(pos.first, pos.second);
}
void print_books(multimap<string, string> &books){
	cout << "当前书目包括:" << endl;
	for (auto &book : books)
		cout << book.first << ",《" << book.second << "》" << endl;
	cout << endl;
}
int main(){
	multimap<string, string> books;
	books.insert({ "Barth, John", "Sot-Weed Factor" });
	books.insert({ "Barth, John", "Lost in the Funhouse" });
	books.insert({ "金庸", "射雕英雄传" });
	books.insert({ "金庸", "天龙八部" });
	print_books(books);
	remove_author(books, "张三");
	remove_author(books, "Barth, John");
	print_books(books);
	system("pause");
	return 0;
}

在这里插入图片描述

练习11.32

使用上一题定义的 multimap 编写一个程序,按字典序打印作者列表和他们的作品。

解:

multimap 的数据结构是红黑树,它维护了元素的关键字的默认序。例如,对字符串关键字(作者),红黑树会维护它们的字典序。当我们遍历 multimap(如遍历 [begin(), end()),或更简单地使用范围 for)时,就是按关键字的字典序来访问元素。

因此,上一题的 print_books 实际上已经实现了按字典序打印作者列表和他们的作品。

但是,当我们要求的不是关键字的默认序(运算符 < 定义的顺序)时,就要复杂一些。由于 sort 算法要求给定的两个迭代器是随机迭代器,关联容器的迭代器不符合这一要求,所以不能直接对其使用 sort 算法。其实这不难理解,关联容器的根本特征就是维护了关键字的默认序,从而实现了按关键字的插入、删除和查找。是不可能通过 sort 使其内部元素呈现出另外一种顺序的。只有本身不关心元素值的顺序容器,才可能随意安排元素顺序(位置)。我们可以在定义 multimap 时使用自己定义的比较操作所定义的关键字的序,而不是使用 < 定义的序,但这只是令 multimap 以另外一种序来维护关键字,仍然不可能在使用 multimap 的过程中来改变关键字顺序。为此,我们只能将 multimap 中的元素拷贝到一个顺序容器(如 vector)中,对顺序容器执行 sort 算法,来获得关键字的其他序。

练习11.33

实现你自己版本的单词转换程序。

解:

#include <map>
#include <vector>
#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
#include <sstream>
using namespace std;
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
	}
}

练习11.34

如果你将 transform 函数中的 find 替换为下标运算符,会发生什么情况?

解:

如前所述,find 仅查找给定关键字在容器中是否出现,若容器中不存在给定关键字,它返回尾后迭代器。当关键字存在时,下标运算符的行为与 find 类似,但当关键字不存在时,它会构造一个 pair(进行值初始化),将其插入到容器中。对于单词转换程序,这会将不存在的内容插入到输出文本中,这显然不是我们所期望的。

练习11.35

buildMap 中,如果进行如下改写,会有什么效果?

trans_map[key] = value.substr(1);
//改为
trans_map.insert({key, value.substr(1)});

解:

map 中没有给定关键字时,insert 操作与 下标操作+赋值 操作的效果类似,都是将关键字和值的 pair 添加到 map 中。

但当 map 中已有给定关键字,也就是新的转换规则与一条已有规则要转换同一个单词时,两者的行为是不同的。

  • 下标操作会获得具有该关键字的元素(也就是已有规则)的值,并将新读入的值赋予它,也就是用新读入的规则覆盖了容器中的已有规则。
  • insert 操作遇到关键字已存在的情况,则不会改变容器内容,而是返回一个值指出插入失败。

因此,当规则文件中存在多条规则转换相同单词时,下标+赋值 的版本最终会用最后一条规则进行文本转换,而 insert 版本则会用第一条规则进行文本转换。

练习11.36

我们的程序并没检查输入文件的合法性。特别是,它假定转换规则文件中的规则都是有意义的。如果文件中的某一行包含一个关键字、一个空格,然后就结束了,会发生什么?预测程序的行为并进行验证,再与你的程序进行比较。

解:

此题有误,书中程序已经处理了这种情况。

buildMap 函数中,当循环中读入要转换的单词和转换的内容后,会检查是否存在转换的内容(value.size()>1),若不存在,则抛出一个异常。

练习11.37

一个无序容器与其有序版本相比有何优势?有序版本有何优势?

解:

无序版本通常性能更好,使用也更为简单。有序版本的优势是维护了关键字的序。

当元素的关键字类型没有明显的序关系,或是维护元素的序代价非常高时,无序容器非常有用。但当应用要求必须维护元素的序时,有序版本就是唯一的选择。

练习11.38

unordered_map 重写单词计数程序和单词转换程序。

解:

对单词计数程序仅有的两处修改是将包含的头文件 map 改为 unordered_map,以及将 word_count 的类型由 map 改为 unordered map

尝试编译、运行此程序,你会发现,由于无序容器不维护元素的序,程序的输出结果与第3题的输出结果的顺序是不同的。

#include <iostream>
#include <string>
#include <algorithm>
#include <map>
#include <fstream>          // file I/O support
#include <cstdlib>          // support for exit()
using namespace std;
int main()
{
	char filename[20];
	cout << "Enter name of data file: ";
	cin.getline(filename, 20);
	ifstream inFile;        // object for handling file input
	inFile.open(filename);  // associate inFile with a file
	if (!inFile.is_open())  // failed to open file
	{
		cout << "Could not open the file " << filename << endl;
		cout << "Program terminating.\n";
		// cin.get();    	// keep window open
		exit(EXIT_FAILURE);
	}
	map<string, size_t> word_count;
	string word;
	while (inFile >> word)
	{
		++word_count[word];
	}
	for (const auto &w : word_count){
		cout << w.first << "出现了" << w.second << "次" << endl;
	}
	inFile.close();         // finished with the file

	system("pause");
	return 0;
}

在这里插入图片描述
在这里插入图片描述

单词转换程序的修改类似。由于程序中不再有元素内容的顺序输出,因此输出结果与有序版本没有什么不同。

#include <unordered_map>
#include <vector>
#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>
#include <sstream>
using namespace std;
unordered_map<string, string> buildMap(ifstream &map_file)
{
	unordered_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 unordered_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
	}
}

如果想要更多的资源,欢迎关注 @我是管小亮,文字强迫症MAX~

回复【福利】即可获取我为你准备的大礼,包括C++,编程四大件,NLP,深度学习等等的资料。

想看更多文(段)章(子),欢迎关注微信公众号「程序员管小亮」~

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是管小亮

一口吃掉你的打赏,嗝~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值