ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

第11 章 关联容器

2022-01-21 17:31:36  阅读:120  来源: 互联网

标签:11 容器 word map 元素 关联 关键字 pair


第11 章 关联容器

关联容器和顺序容器有着根本的不同:关联容器中的元素是按关键字来保存和访问的。与之相对,顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。

虽然关联容器的很多行为与顺序容器相同,但其不同之处反映了关键字的作用。

关联容器支持高效的关键字查找和访问。两个主要的关联容器(associative-container)类型是 mapset。map 中的元素是一些关键字-值(key-value)对:关键字起到索引的作用,值则表示与索引相关联的数据。set 中每个元素只包含一个关键字:set 支持高效的关键字查询操作——检查一个给定关键字是否在 set 中。例如,在某些文本处理过程中,可以用一个 set 来保存想要忽略的单词。字典则是一个很好的使用 map 的例子:可以将单词作为关键字,将单词释义作为值。

标准库提供 8 个关联容器,如下表所示。

关联容器类型
按关键字有序保存元素
map关联数组:保存关键字-值对
set关键字即值,即只保存关键字的容器
multimap关键字可重复出现的 map
multiset关键字可以重复出现的 set
无序集合
unordered_map用哈希函数组织的 map
unordered_set用哈希函数组织的 set
unordered_multimap哈希组织的 map:关键字可以重复出现
unordered_multiset哈希组织的 set:关键字可以重复出现

11.1 使用关联容器

map 是关键字-值对的集合。例如,可以将一个人的名字作为关键字,将其电话号码作为值。我们称这样的数据结构为“将名字映射到电话号码“。map 类型通常被称为关联数组(associative array)。关联数组与“正常”数组类似,不同之处在于其下标不必是整数。我们通过一个关键字而不是位置来查找值。给定一个名字到电话号码的 map,我们可以使用一个人的名字作为下标来获取此人的电话号码。

与之相对,set 就是关键字的简单集合。当只是想知道一个值是否存在时,set 是最有用的。例如,一个企业可以定义一个名为 bad_checks 的 set 来保存那些曾经开过空头支票的人的名字。在接受一张支票之前,可以查询 bad_checks 来检查顾客的名字是否在其中。

使用 map

一个经典的使用关联数组的例子是单词计数程序:

// 统计每个单词在输入中出现的次数
void count_word()
{
	map<string, int> word_count;	// string 到 int 的空 map
	string word;
	while (cin>>word)
	{
		++word_count[word];			// 提取 word 的计数器并将其加一
	}
	for (const auto &w : word_count)// 对 map 中的每个元素
	{
		// 打印结果
		cout << w.first << " occurs " << w.second
			<< ((w.second > 1) ? " times" : " time") << endl;
	}
}

此程序读取输入,报告每个单词出现多少次。

类似顺序容器,关联容器也是模板。为了定义一个 map,我们必须指定关键字和值的类型。在此程序中,map 保存的每个元素中,关键字是 string 类型,值是 int 类型。当对 word_count 进行下标操作时,我们使用一个 string 作为下标,获得与此 string 相关联的 int 类型的计数器。

使用 set

上一个示例程序的一个合理扩展是:忽略常见单词,如 “the”、“and”、"or"等。我们可以使用 set 保存想忽略的单词,只对不出现在集合中的单词统计出现次数。

// 统计每个单词在输入中出现的次数
void count_word1()
{
	map<string, int> word_count;	// string 到 int 的空 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 的计数器并将其加一
		}	
	}
	for (const auto &w : word_count)// 对 map 中的每个元素
	{
		// 打印结果
		cout << w.first << " occurs " << w.second
			<< ((w.second > 1) ? " times" : " time") << endl;
	}
}

与其他容器类似,set 也是模板。为了定义一个 set ,必须指明其元素类型,本例中是 string。与顺序容器类似,可以对一个关联容器的元素进行列表初始化(9.2.4节)。集合 exclude 保存了 12 个我们想忽略的单词。

此程序与前一个程序的重要不同是,在统计每个单词出现次数之前,我们检查单词是否在忽略集合中,这是在 if 语句中完成的:

if (exclude.find(word) == exclude.end())

find 调用返回一个迭代器。如果给定关键字在 set 中,迭代器指向该关键字。否则,find 返回尾后迭代器。在此程序中,仅当 word 不在 exclude 中时我们才更新 word 的计数器。

11.1 节练习

练习 11.1:描述 map 和 vector 的不同。

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

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

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

练习 11.2:分别给出最适合使用 list、vector、deque、map 以及 set 的例子。

若元素很小(例如 int ),大致数量预先可知,在程序运行过程中不会剧烈变化。大部分情况下只在末尾添加或删除需要频繁访问任意位置的元素,则 vector 可带来最高的效率。若需频繁在头部和尾部添加或删除元素,则 deque 是最好的选择。

如果元素较大(如大的类对象),数量预先不知道,或是程序运行过程中频繁变化,对元素的访问更多是顺序访问全部或很多元素,则 list 很适合。

map 很适合对一些对象按它们的某个特征进行访问的情形。典型的例如按学生的名字来查询学生信息,即可将学生名字作为关键字,将学生信息作为元素值,保存在 map 中。

set,顾名思义,就是集合类型。当需要保存特定的值集合——通常满足/不满足某种要求的值集合,用 set 最方便。

// 练习 11.3:编写你自己的单词计数程序
void count_word()
{
	map<string, int> word_count;	// string 到 int 的空 map
	string word;
	while (cin >> word)
	{
		++word_count[word];			// 提取 word 的计数器并将其加一
	}
	for (const auto &w : word_count)// 对 map 中的每个元素
	{
		// 打印结果
		cout << w.first << " 出现了 " << w.second
			<< " 次" << endl;
	}
}
// 练习 11.4:扩展你的程序,忽略大小写和标点。例如,“example."、"example,"和”Example"应该递增相同的计数器。

// 编写函数 trans,将单词中的标点去掉,将大写转换为小写
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;
}
void count_word1()
{
	map<string, int> word_count;	// string 到 int 的空 map

	string word;
	while (cin >> word)
	{
		++word_count[trans(word)];			// 获取 word 的计数器并将其加一
	}
	for (const auto &w : word_count)// 对 map 中的每个元素
	{
		// 打印结果
		cout << w.first << " 出现了 " << w.second
			<< " 次" << endl;
	}
}

11.2 关联容器概述

关联容器(有序的和无序的)都支持9.2节中介绍的普通容器操作。关联容器不支持顺序容器的位置相关的操作,例如 push_front 或 push_back。原因是关联容器中元素是根据关键字存储的,这些操作对关联容器没有意义。而且,关联容器也不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。

除了与顺序容器相同的操作外,关联容器还支持一些顺序容器不支持的操作。此外,无序容器还提供一些用来调整哈希性能的操作。

关联容器的迭代器都是双向的。

11.2.1 定义关联容器

如前所示,当定义一个 map 时,必须既指明关键字类型又指明值类型;而定义一个 set 时,只需指明关键字类型,因为 set 中没有值。每个关联容器都定义了一个默认构造函数,它创建一个指定类型的空容器。在新标准下,我们也可以对关联容器进行值初始化。

	map<string, int> words_count;	// 空容器
	// 列表初始化
	set<string> exclude = { "the","but","or","an","a",
							"The","But","And","Or","An","A"};
	// 三个元素;autuors 将姓映射为名
	map<string, string> authors = { {"Joyce","James"},
								{"Austen","Jane"},
								{"Dickens","Charles"} };

与以往一样,初始化器必须能转换为容器中元素的类型。对于 set,元素类型就是关键字类型。

当初始化一个 map 时,必须提供关键字类型和值类型。我们将每个关键字-值对包围在花括号中:

{key,value}

来指出它们一起构成了 map 中的一个元素。在每个花括号中,关键字是第一个元素,值是第二个。因此 authors 将姓映射到名,初始化后它包含三个元素。

初始化 multimap 和 multiset

一个 map 或 set 中的关键字必须是唯一的,即,对于一个给定的关键字,只能有一个元素的关键字等于它。容器 multimap 和 multiset 没有此限制,它们都允许多个元素具有相同的关键字。例如,在我们用来统计单词数量的 map 中,每个单词只能有一个元素。另一方面,在一个词典中,一个特定单词则可具有多个与之关联的词义。

下面的例子展示了具有唯一关键字的容器与允许重复关键字的容器之间的区别。首先,我们将创建一个名为 ivec 的保存 int 的 vector,它包含 20 个元素:0 到 9 每个整数有两个拷贝。我们将使用此 vector 初始化一个 set 和一个 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

image-20220117114954819

11.2.1 节练习

练习 11.5:解释 map 和 set 的区别。你如何选择使用哪个?

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

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

练习 11.6:解释 set 和 list 的差别。你如何选择使用哪个?

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

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

// 练习 11.7:定义一个 map,关键字是家庭的姓,值是一个 vector,保存家中孩子(们)
// 的名。编写代码,实现添加新的家庭以及向已有家庭中添加新的孩子。
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);
}
void test_family()
{
	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;
	}
}

image-20220117134852046

// 练习 11.8:编写一个程序,在一个 vector 而不是一个 set 中保存不重复的单词。使用 set 的优点是什么?
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);
	}
}
// 使用 vector
void save_words_toV()
{
	vector<string> unique_word;
	string word;
	while (cin >> 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 << " ";
	}
	cout << endl;
}

// 使用 set
void save_words_toSet()
{
	set<string> unique_word;
	string word;
	while (cin >> word)
	{
		trans(word);
		unique_word.insert(word);			//  添加不重复单词
	}
	for (const auto &w : unique_word)		// 打印不重复单词
	{
		// 打印结果
		cout << w << " ";
	}
	cout << endl;
}

使用 set 可以不用编写检查重复单词的代码。vector 可以保持单词的输入顺序,而 set 则不能,遍历 set,元素是按值的升序被遍历的。

11.2.2 关键字类型的要求

关联容器对其关键字类型有一些限制。对于有序容器——map、multimap、set 以及 multiset,关键字类型必须定义元素比较的方法。默认情况下,标准库使用关键字类型的 < 运算符来比较两个关键字。在集合类型中,关键字类型就是元素类型;在映射类型中,关键字类型是元素的第一部分的类型。

传递给排序算法的可调用对象必须满足与关联容器中关键字一样的类型要求。

有序容器的关键字类型

可以向一个算法提供我们自己定义的比较操作(10.3节),与之类似,也可以提供自己定义的操作来代替关键字上的 < 运算符。所提供的操作必须在关键字类型上定义一个严格弱序(strict weak ordering)。可以将严格弱序看作“小于等于”。它必须具备如下基本性质:

  • 两个关键字不能同时“小于等于”对方;如果 k1 “小于等于” k2,那么 k2 绝不能“小于等于” k1。
  • 如果 k1 “小于等于” k2,且 k2 “小于等于” k3,那么 k1 必须 “小于等于” k3。
  • 如果存在两个关键字,任何一个都不“小于等于” 另一个,那么我们称这两个关键字是“等价”的。如果 k1 “等价于” k2,且 k2 “等价于” k3,那么 k1 必须“等价于” k3。

如果两个关键字是等价的(即,任何一个都不“小于等于”另一个),那么容器将它们视作相等来处理。当用作 map 的关键字时,只能有一个元素与这两个关键字关联,我们可以用两者中任意一个来访问对应的值。

使用关键字类型的比较元素

用来组织一个容器中元素的操作也是该容器类型的一部分。为了指定使用自定义的操作,必须在定义关联容器类型时提供此操作的类型。如前所述,用尖括号指出要定义哪种类型的容器,自定义的操作类型必须在尖括号中紧跟着元素类型给出。

在尖括号中出现的每个类型,就仅仅是一个类型而已。当我们创建一个容器(对象)时,才会以构造函数参数的形式提供真正的比较操作(其类型必须与在尖括号中指定的类型相吻合)。

例如,我们不能直接定义一个 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,以及比较操作类型——应该是一种函数指针类型(6.7节),可以指向 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.2 节练习

// 练习 11.9:定义一个 map,将单词与一个行号的 list 关联,list 中保存的是单词所出现的行号。
map<string, list<int>>word_lineno;
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);
	}
}
void save_word()
{
	map<string, list<int>>word_lineno;	//单词到行号的映射
	string line;
	string word;
	int lineno = 0;
	ifstream in;
	if (!in)
	{
		cout << "打开输入文件失败!" << endl;
	}
	while (getline(in, 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;
	}
}

练习 11.10:可以定义一个 vector::iterator 到 int 的 map 吗?list::iterator 到 int 的 map 呢?对于两种情况,如果不能,解释为什么

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

map<vector<int>::iterator, int> m1;

是可以的,因为 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.2.3 pair 类型

在介绍关联容器的操作之前,我们需要了解名为 pair 的标准库类型,它定义在头文件 utility 中。

一个 pair 保存两个数据成员。类似容器,pair 是一个用来生成特定类型的模板。当创建一个 pair 时,我们必须提供两个类型名,pair 的数据成员将具有对应的类型。两个类型不要求一样:

pair<string, string> anon;			// 保存两个 string
pair<string, size_t> word_count;	// 保存一个 string 和一个 size_t
pair<string, vector<int>> line;		// 保存 string 和 vector<int>

pair 的默认构造函数对数据成员进行值初始化(参见3.3.1节)。因此,anon 是一个包含两个空 string 的 pair,line 保存一个空 string 和一个空 vector。word_count 中的 size_t 成员值为 0,而 string 成员被初始化为空。

我们也可以为每个成员提供初始化器:

pair<string, string> author{"James","Joyce"};

这条语句创建一个名为 author 的 pair ,两个成员被初始化 为"James"和"Joyce"。

与其他标准库类型不同,pair 的数据成员是 public 的(7.2节)。两个分别命名为 first 和 second。我们用普通的额乘以访问符号(1.5.2节)来访问它们,例如,在之前的单词计数程序的输出语句中我们就是这么做的:

// 打印结果
cout<< w.first << " occurs " << w.second
	<<((w.second > 1) ? "times" : "time") << endl;

此处,w 是指向 map 中某个元素的引用。map 的元素是 pair。下表列出了 pair 上的操作。

pair 上的操作
操作描述
pair<T1, T2> p;p 是一个 pair,两个类型分别为 T1 和 T2 的成员都进行了值初始化(3.3.1节)
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 对象的函数

想象有一个函数需要返回一个 pair。在新标准下,我们可以对返回值进行列表初始化(6.3.2节)

// 一个返回 pair 的函数
pair<string,int> process(vector<string> &v)
{
	// 处理 v
	if (!v.empty())
	{
		return { v.back(),v.back().size() };	//列表初始化
	}
	else
	{
		return pair<string, int>();				// 隐式构造返回值
	}
}

若 v 不为空,我们返回一个由 v 中最后一个 string 及其大小组成的 pair。否则,隐式构造一个空 pair,并返回它。

在较早的 C++ 版本中,不允许用花括号包围的初始化器来返回 pair 这种类型的对象,必须显式构造返回值:

	if (!v.empty())
	{
		return pair<string, int>(v.back(),v.back().size());	
	}

我们还可以用 make_pair 来生成 pair 对象,pair 的两个类型来自于 make_pair 的参数:

	if (!v.empty())
	{
		return make_pair(v.back(),v.back().size());	
	}

11.2.3 节练习

// 练习 11.12:编写程序,读入 string 和 int 的序列,将每个 string 和 int 存入一个 pair 中,pair 保存在一个 vector 中。
void save_to_pair()
{
	string s;
	int i;
	vector<pair<string, int>> v;
	while (cin >> s && cin >> i)
	{
		v.push_back(pair<string, int>(s, i));;
	}
	// 打印数据
	for (const auto &d : v)
		cout << d.first << " " << d.second << endl;
}
// 练习 11.13:编写上一题程序的三个版本,分别采用不同的方法创建 pair。解释你认为哪种形式最易于编写和理解,为什么?
void save_to_pair1()
{
	string s;
	int i;
	vector<pair<string, int>> v;
	while (cin >> s && cin >> i)
	{
		//第一种	默认初始化
		v.push_back(pair<string, int>(s, i));
		//第二种	列表初始化
		v.push_back({ s,i });
		//第三种
		v.push_back(make_pair(s, i));

	}
	// 打印数据
	for (const auto &d : v)
		cout << d.first << " " << d.second << endl;
}
// 练习 11.14: 扩展 11.2.1 中编写的孩子姓到名的 map,添加一个 pair 的 vector,保存孩子的名和生日。
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 });
}
void test_family()
{
	// vector 中保存 pair
	map<string, vector<pair<string, string>>> families;

	add_family(families, "张");
	add_child(families, "张","强","2000-1-1");
	add_child(families, "张", "刚","1980-2-2");
	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;
	}
}

image-20220118140749275

11.3 关联容器操作

关联容器额外的类型别名
类型别名描述
key_type此容器类型的关键字类型
mapped_type每个关键字关联的类型;只适用于 map
value_type对于 set,与 key_value 相同; 对于 map,为 pair<const key_type, mapped_type>

对于 set 类型,key_typevalue_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

image-20220118142712210

与顺序容器一样(9.2.2节),我们使用作用域运算符来提取一个类型的成员——例如,map<string, int>::key_type。

只有 map 类型(unordered_map、unordered_multimap、multimap 和 map )才定义了 mapped_type

11.3.1 关联容器迭代器

当解引用一个关联容器迭代器时,我们会得到一个类型为容器的 value_type 的值的引用。对 map 而言,value_type 是一个 pair 类型,其 first 成员保存 const 的关键字,second 成员保存值:

	map<string, int> word_count;
	// 获得指向 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;				// 正确:我们可以通过迭代器改变元素

必须记住,一个 map 的 value_type 是一个 pair,我们可以改变 pair 的值,但不能改变关键字成员的值。

set 的迭代器是 const 的

虽然 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 和 set 都支持 begin 和 end 操作。与往常一样,我们可以用这些函数获取迭代器,然后用迭代器来遍历容器。例如,我们可以编写一个循环来打印单词计数程序的结果,如下

	map<string, int> word_count;
	// 获得指向 word_count 中一个元素的迭代器
	auto map_it = word_count.cbegin();
	// 比较当前迭代器和尾后迭代器
	while (map_it != word_count.cend())
	{
		// 解引用迭代器,打印关键字-值对
		cout << map_it->first << " occurs "
			<< map_it->second << "times" << endl;
		++map_it;	//递增迭代器,移动到下一个元素
	}

本程序的输出是按字典序排列的。当使用一个迭代器遍历一个 map、multimap、set 或 multiset 时,迭代器按关键字升序遍历元素。

关联容器和算法

我们通常不对关联容器使用泛型算法(参见第 10 章)。关键字是 const 这一特性意味着不能将关联容器传递给修改或重排容器元素的算法,因为这类算法需要向元素写入值,而 set 类型中的元素是 const 的,map 中的元素是 pair,其第一个成员是 const 的。

关联容器可用于只读取元素的算法。但是,很多这类算法都有搜索序列。由于关联容器中的元素不能通过它们的关键字进行(快速)查找,因此对其使用泛型搜索算法几乎总是个坏主意。

在实际编程中,如果我们真要对一个关联容器使用算法,要么是将它当作一个序列,要么当作一个目的位置。例如,可以用泛型 copy 算法将元素从一个关联容器拷贝到另一个序列。类似的,可以调用 inserter 将一个插入器绑定(10.4.1节)到一个关联容器。通过使用 inserter,我们可以将关联容器当作一个目的位置来调用另一个算法。

11.3.1 节练习

练习 11.15:对一个 int 的 vector 的 map,其 mapped_type、key_type 和 value_type 分别是什么?

vector; int ; pair<const int, vector>.

// 练习 11.16:使用一个 map 迭代器编写一个表达式,将一个值赋予一个元素。
void map_it()
{
	map<string, int> mp;
	auto it = mp.begin();
	it->second = 0;
}
// 练习 11.17:假定 c 是一个 string 的 multiset,v 是一个 string 的 vector,
// 解释下面的调用。指出每个调用是否合法:
void test()
{
	multiset<string> c;
	vector<string> v;
	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.19:定义一个变量,通过对 11.2.2 节中的名为 bookstore 的 multiset 调用 begin() 来初始化这个变量。写出变量的类型,不要使用 auto 或 decltype。

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.3.2 添加元素

关联容器的 insert 成员向容器中添加一个元素或一个元素范围。由于 map 和 set (以及对应的无序类型)包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响:

vector<int> ivec={2,4,6,8,2,4,6,8};
	set<int> set2;								// 空集合
	set2.insert(ivec.cbegin(), ivec.cend());	// set2 有 4 个元素
	cout << endl;
	set2.insert({ 1,3,5,7,1,3,5,7 });			// set2 现在有 8 个元素

image-20220119094028215

insert 有两个版本,分别接受一对迭代器,或是一个初始化列表,这两个版本的行为类似对应的构造函数(11.2.1节)——对于一个给定的关键字,只有第一个带此关键字的元素才被插入到容器中。

向 map 添加元素

向一个 map 进行 insert 操作时必须记住元素类型是 pair 。通常,对于想要插入的数据,并没有一个现成的 pair 对象。可以在 insert 的参数列表中创建一个 pair:

	map<string, int> word_count;
	string word;
	// 向 word_count 插入 word 的 4 种方法
	word_count.insert({ word,1 });
	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));

如我们所见,在新标准下,创建一个 pair 最简单的的方法是在参数列表中使用花括号初始化。也可以调用 make_pair 或显式构造 pair。最后一个 insert 调用中的参数:

map<string, int>::value_type(s, 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 作为一个提示,指出从哪里开始搜索新元素应该存储的位置。返回一个迭代器,指向具有给定关键字的元素

检测 insert 的返回值

insert(或 emplace)返回的值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的 insert 和 emplace 版本返回一个 pair,告诉我们插入操作是否成功。pair 的 first 成员是一个迭代器,指向具有给定关键字的元素;second 成员是一个 bool 值,指出元素是插入成功还是以及存在于容器中。如果关键字已在容器中,则 insert 什么事情也不做,且返回值中的 bool 部分为 false。如果关键字不存在,则元素被插入容器中,且 bool 值为 true。

作为一个例子,我们用 insert 重写单词计数程序:

// 单词计数程序
void word_count()
{
	// 统计每个单词在输入中出现次数的一种更烦琐的方法
	map<string, int>word_count;
	string word;
	while (cin >> word)
	{
		// 插入一个元素,关键字等于 word,值为 1;
		// 若 word 已在 word_count 中,insert 什么也不做
		auto ret = word_count.insert({ word,1 });
		if (!ret.second)			// word 已在 word_count 中
		{
			++ret.first->second;	// 递增计数器
		}
	}
}

对于每个 word,我们尝试将其插入到容器中,对应的值为 1。若 word 已在 map 中,则什么都不做,特别是与 word 相关联的计数器的值不变。若 word 还未在 map 中,则此 string 对象被添加到 map 中,且计数器的值被置为 1。

if 语句检查返回值的 bool 部分,若为 false,则表明插入操作未发生。在此情况下,word 已存在于 word_count 中,因此必须递增此元素所关联的计数器。

展开递增语句

在这个版本的单词计数程序中,递增计数器的语句很难理解。通过添加一些括号来反映出运算符的优先级,使表达式更容易理解:

++((ret.first)->second);	// 等价表达式

下面我们一步一步来解释此表达式:

ret 保存 insert 返回的值,是一个 pair

ret.first 是 pair 的第一个成员,是一个 map 迭代器,指向具有给定关键字的元素

ret.first-> 解引用此迭代器,提取 map 中的元素,元素也是一个 pair

ret.first->second map 中元素的值部分

++ret.first->second 递增此值

向 multiset 或 multimap 添加元素

我们的单词计数程序依赖于这样一个事实:一个给定的关键字只能出现一次。这样任意给定的单词只有一个关联的计数器。我们有时希望能添加具有相同关键字的多个元素。例如,可能想建立作者到他所著书籍题目的映射。在此情况下,每个作者可能有多个条目,因此我们应该用 multimap 而不是 map。由于一个 multi 容器中的关键字不必唯一,在这些类型上调用 insert 总会插入一个元素:

	multimap<string, string> authors;
	// 插入第一个元素,关键字为 Barth,Jhon
	authors.insert({ "Barth,Jhon","Sot-Weed Factor" });
	// 正确:添加第二个元素,关键字也是 Barth,Jhon
	authors.insert({ "Barth,Jhon","Lost in the Funhouse" });
	for (auto value : authors)
	{
		cout << value.first << " " << value.second << endl;
	}
}

image-20220119112705084

对允许重复关键字的容器,接受单个元素的 insert 操作返回一个指向新元素的迭代器。这里无须返回一个 bool 值,因为 insert 总是向这类容器中加入一个新元素。

11.3.2 节练习

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

// 统计每个单词在输入中出现的次数
void count_word()
{
	map<string, int> word_count;	// string 到 int 的空 map
	string word;
	while (cin>>word)
	{
		++word_count[word];			// 提取 word 的计数器并将其加一
	}
	for (const auto &w : word_count)// 对 map 中的每个元素
	{
		// 打印结果
		cout << w.first << " occurs " << w.second
			<< ((w.second > 1) ? " times" : " time") << endl;
	}
}

使用下标操作更简洁易读

练习 11.21:假定 word_count 是一个 string 到 int 的 map ,word 是一个 string,解释下面循环的作用:

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

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

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

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

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

参数类型是一个 pair, first 成员的类型是 map 的关键字类型 string,second 成员的类型是 map 的值的类型 vector:

pair<string,vector>

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

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

// 练习 11.23:11.2.1 节练习中的 map 以孩子的姓为关键字,保存他们的名的 vector,用 multimap 重写此 map。
void add_child(multimap<string, string> &families, const string &family, const string &child)
{
	families.insert({ family,child });

}
void test_family()
{
	multimap<string, string>  families;
	add_child(families, "张", "强");
	add_child(families, "张", "刚");
	add_child(families, "王", "五");
	for (auto f : families)
	{
		cout << f.first << "家的孩子: " << f.second << endl;
	}
}
i

image-20220119141456745

11.3.3 删除元素

关联容器定义了三个版本的 erase。如下表所示。与顺序容器一样,我们可以通过传递给 erase 一个迭代器或一对迭代器来删除一个元素或一个元素范围。这两个版本的 erase 与对应的顺序容器的操作非常相似:指定的元素被删除,函数返回 void。

关联容器提供一个额外的 erase 操作,它接受一个 key_type 参数。此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除的元素的数量。我们可以用此版本在打印结果之前从 word_count 中删除一个特定的单词:

	// 删除一个关键字,返回删除的元素数量
	if (word_count.erase(remove_word))
		cout << "OK: " << remove_word << " removed\n";
	else cout << "oops: " << remove_word << " not found!\n";

对于保存不重复关键字的容器,erase 的返回值总是 0 或 1.若返回值为 0,则表明想要删除的元素并不在容器中。

对允许重复关键字的容器,删除元素的数量可能大于 1:

auto cnt = authors.erase("Barth,Jhon");

如果 authors 是 11.3.2 节中创建的 multimap,则 cnt 的值为2。

从关联容器删除元素
操作描述
c.erase(k)从 c 中删除每个关键字为 k 的元素。返回一个 size_type 的值,指出删除的元素的数量
c.erase§从 c 中删除迭代器 p 指定的元素。p 必须指向 c 中一个真实元素,不能等于 c.end()。返回一个指向 p 之后元素的迭代器,若 p 指向 c 中的尾元素,则返回 c.end()
c.erase(b,e)删除迭代器 b 和 e 所表示的范围中的元素。返回 e

11.3.4 map 的下标操作

map 和 unordered_map 容器提供了下标运算符和一个对应的 at 函数,如下表所示。set 类型不支持下标,因为 set 中没有与关键字相关联的“值”。元素本身就是关键字,因此“获取与一个关键字相关联的值”的操作就没有意义了。我们不能对一个 multimap 或一个 unordered_multimap 进行下标操作,因为这些容器中可能有多个值与一个关键字相关联。

类似我们用过的其他下标运算符,map 下标运算符接受一个索引(即,一个关键字),获取与此关键字相关联的值。但是,与其他下标运算符不同的是,如果关键字并不在 map 中,会为它创建一个元素并插入到 map 中,关联值将进行值初始化(3.3.1 节)。

例如,如果我们编写如下代码

map<string, int> word_count;
// 插入一个关键字为 Anna 的元素,关联值进行值初始化;然后将 1 赋予它
word_count["Anna"] = 1;

将会执行如下操作:

  • 在 word_count 中搜索关键字为 Anna 的元素,未找到。
  • 将一个新的关键字-值对插入到 word_count 中。关键字是一个 const_string,保存 Anna。值进行值初始化,在本例中意味着值为 0.
  • 提取出新插入的元素,并将值 1 赋予它。

由于下标运算符可能插入一个新元素,我们只可以对非 const 的 map 使用下标操作。

对一个 map 使用下标操作很不相同:使用一个不在容器中的关键字作为下标,会添加一个具有此关键字的元素到 map 中。

map 和 unordered_map 的下标操作
操作描述
c[k]返回关键字为 k 的元素;如果 k 不在 c 中,添加一个关键字为 k 的元素,对其进行值初始化
c.at(k)访问关键字为 k 的元素,带参数检查;若 k 不在 c 中,抛出一个 out_of_range 异常

使用下标操作的返回值

map 的下标运算符与我们用过的其他下标运算符的另一个不同之处是其返回类型。通常情况下,解引用一个迭代器所返回的类型与下标运算符返回的类型是一样的。但对 map 则不然:当对一个 map 进行下标操作时,会获得一个 mapped_type 对象;但当解引用一个 map 迭代器时,会得到一个 value_type 对象(11.3节)。

与其他下标运算符相同的是,map 的下标运算符返回一个左值(4.1.1节)。由于返回的是一个左值,所以我们既可以读也可以写元素:

cout<<word_count["Anna"];// 用 Anna 作为下标提取元素;会打印出 1
++word_count["Anna"];	// 提取元素,将其增1
cout<<word_count["Anna"];//提取元素并打印它;会打印出 2

11.3.4 节练习

练习 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<string, int> 用来进行下标操作的类型是 string,下标操作返回的类型是 int。

11.3.5 访问元素

关联容器提供多种查找一个指定元素的方法,如下表所示。如果我们所关心的只不过是一个特定元素是否已在容器中,可能 find 是最佳选择。对于不允许重复关键字的容器,可能使用 find 还是 count 没什么区别。但是对于运行重复关键字的容器,count 还会做更多工作:如果元素在容器中,它还会统计有多少个元素有相同的关键字。如果不需要计数,最好使用 find:

	set<int> iset = { 0,1,2,3,4,5,6,7,8,9 };
	iset.find(1);		// 返回一个迭代器,指向 key == 1 的元素
	iset.find(11);		// 返回一个迭代器,其值等于 iset.end()
	iset.count(1);		// 返回 1
	iset.count(0);		// 返回 0
在一个关联容器中查找元素的操作
操作描述
lower_bound 和 upper_bound 不适用于无序容器
下标 和 at 操作只适用于非 const 的 map 和 unordered_map
c.find(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()

对 map 使用 find 代替下标操作

对 map 和 unordered_map 类型,下标运算符提供了最简单的提取元素的方法。但是,使用下标操作有一个严重的副作用:如果关键字还未在 map 中,下标操作会插入一个具有给定关键字的元素。

但有时,我们只是想知道一个给定元素是否在 map 中,而不想改变 map。在这种情况下,应该使用 find:

if(word_count.find("floor")==word_count.end())
	cout<<"floor is not in the map"<<endl;

在 multimap 或 multiset 中查找元素

对于允许重复关键字的容器来说,查找一个元素的过程更为复杂:在容器中可能有很多元素具有给定的关键字。如果一个 multimap 或 multiset 中有多个元素具有给定关键字,则这些元素在容器中会相邻存储。

例如,给定一个作者到著作题目的映射,我们可能想点一个特定作者的所有著作。可以用三种不同的方法来解决这个问题。最直观的方法是使用 find 和 count:

	multimap<string, string> authors;
	// 插入第一个元素,关键字为 Barth,Jhon
	authors.insert({ "Barth,Jhon","Sot-Weed Factor" });
	// 正确:添加第二个元素,关键字也是 Barth,Jhon
	authors.insert({ "Barth,Jhon","Lost in the Funhouse" });	
	// 打印一个特定作者的所有著作
	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;								// 记录已经打印了多少本书
	}

首先调用 count 确定此作者共有多少本著作,并调用 find 获得一个迭代器,指向第一个关键字为此作者的元素。while 循环的迭代次数依赖于 count 的返回值。特别是,如果 count 返回 0,则循环一次也不执行。

一种不同的,面向迭代器的解决方法

我们还可以用 lower_bound 和 upper_bound 来解决此问题。这两个操作都接受一个关键字,返回一个迭代器。如果关键字在容器中,lower_bound 返回的迭代器将指向第一个具有给定关键字的元素,而 upper_bound 返回的迭代器则指向最后一个匹配给定关键字的元素之后的位置。如果元素不在 multimap 中,则 lower_bound 和 upper_bound 会返回相等的迭代器——指向一个不影响排序的关键字插入位置。因此,用相同的关键字调用 lower_bound 和 upper_bound 会得到一个迭代器范围,表示所有具有该关键字的元素的范围。

当然,这两个操作返回的迭代器可能是容器的尾后迭代器。如果我们查找的元素具有容器中最大的关键字,则此关键字的 upper_bound 返回尾后迭代器。如果关键字不存在,且大于容器中任何关键字,则 lower_bound 返回的也是尾后迭代器。

使用这两个操作,我们可以重写前面的程序:

	// 使用 lower_bound 和 upper_bound 重写前面的程序
	// 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 和 upper_bound 返回相同的迭代器,则给定关键字不在容器中。

equal_range 函数

解决此问题的最后一种方法是三种方法中最直接的:直接调用 equal_range 即可。此函数接受一个关键字,返回一个迭代器 pair。若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。

可以用 equal_range 来再次修改我们的程序:

	// pos 保存迭代器对,表示与关键字匹配的元素范围
	for (auto pos = authors.equal_range(search_item); pos.first != pos.second; ++pos.first)
		cout << pos.first->second << endl;	// 打印每个题目

11.3.5 节练习

// 练习 11.28:对一个 string 到 int 的 vector 的 map,定义并初始化一个变量来保存在其上并调用 find 所返回的结果
map<string, vector<int>> m;
// 保存 find 返回结果的变量
map<string, vector<int>>::iterator iter;

练习 11.29:如果给定的关键字不在容器中,upper_bound、lower_bound 和 equal_range 分别会返回什么?

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

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

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

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


// 练习 11.31:编写程序,定义一个作者及其作品的 multimap。使用 find 在 multimap 中查找一个元素并
// erase 删除它。确保你的程序在元素不在 map 中时也能正常运行。
void remove_author(multimap<string, string> &books, const string &author)
{
	auto pos = books.equal_range(author);
	if (pos.first == pos.second)
	{
		cout << "没有 " << author << " 这个作者" << endl;
	}
	else
	{
		books.erase(pos.first, pos.second);
	}
}
// 练习 11.32:使用上一题定义的 multimap 编写一个程序,按字典序打印作者列表和他们的作品
void print_books(multimap<string, string> &books)
{
	cout << "当前书目包括: " << endl;
	for (auto &book : books)	// 遍历所有书籍,打印之
		cout << book.first << ", 《" << book.second << "》" << endl;
	cout << endl;
}

11.3.6 一个单词转换的 map

我们将以一个程序结束本节的内容,它将展示 map 的创建、搜索以及遍历。这个程序的功能是这样的:给定一个 string,将它转换为另一个 string。程序的输入是两个文件。第一个文件保存的是一些规则,用来转换第二个文件中的文本。每条规则由两部分组成:一个可能出现在输入文件中的单词和一个用来替换它的短语。表达的含义是,每当第一个单词出现在输入中时,我们就将它替换成对应的短语。第二个输入文件包含要转换的文本。

如果单词转换文件的内容如下所示:

brb be right back
k okay?
y why
r are
u you
pic picture
thk thanks!
18r later

我们希望转换的文本为

where r u
y 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 = bulidMap(map_file);		// 保存转换规则
	string text;								// 保存输入中的每一行
	while (getline(input, text))				// 读取一行输入
	{
		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,我们将它保存在 trans_map 中。函数的剩余部分处理输入文件。while 循环用 getline 一行一行地读取文件。这样做的目的是使得输出中的换行位置能和输入文件中一样。为了从每行中获取单词,我们使用了一个嵌套的 while 循环,他用一个 istringstream (8.3节) 来处理当前行中的每个单词。

在输出过程中,内层 while 循环使用一个 bool 变量 firstword 来确定是否打印一个空格。它通过调用 transform 来获得要打印的单词。transform 的返回值或者是 word 中原来的 string,或者是 trans_map 中指出的对应的转换内容。

建立转换映射

函数 buildMap 读入给定文件,建立起转换映射。

map<string,string> bulidMap(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 不会跳过前导空格(3.2.2节),需要我们来跳过单词和它的转换内容之间的空格。在保存转换规则之前,检查是否获得了一个以上的字符。如果是,调用 substr(9.5.1节)来跳过分隔单词及其转换短语之间的前导空格,并将得到的子字符存入 trans_map 。

注意,我们使用下标运算符来添加关键字-值对。我们隐含地忽略了一个单词在转换文件中出现多次的情况。如果真的有单词出现多次,循环会将最后一个对应短语存入 trans_map 。当 while 循环结束后,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;					// 否则返回原 string
	}
}

函数调用 find 来确定给定 string 是否在 map 中。如果存在,则 find 返回一个指向对应元素的迭代器。否则,find 返回尾后迭代器。如果元素存在,我们解引用迭代器,获得一个保存关键字和值的 pair (11.3节),然后返回成员 second,即用来替代 s 的内容。

11.3.6 节练习

练习 11.33:实现你自己版本的单词转换程序。

map<string,string> bulidMap(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;
}
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;					// 否则返回原 string
	}
}
void word_transform(ifstream &map_file, ifstream &input)
{
	auto trans_map = bulidMap(map_file);		// 保存转换规则
	string text;								// 保存输入中的每一行
	while (getline(input, text))				// 读取一行输入
	{
		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;							// 完成一行的转换
	}
}

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

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

练习 11.35:在 buildMap 中,如果进行如下改写,会有什么效果?

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

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

但当 map 中已有给定关键字,也就是新的转换规则与上一条已有规则要转换同一个单词时,两者的行为是不同的。下标操作会获得具有该关键字的元素(也就是已有规则)的值,并将新读入的值赋予它,也就是用心读入的规则覆盖了容器中的已有规则。但 insert 操作遇到关键字已存在的情况,则不会改变容器内容,而是返回一个值指出插入失败。因此,当规则文件中存在多条规则转换相同单词时,下标+赋值的版本最终会用最后一条规则进行文本转换,而 insert 版本则会用第一条规则进行文本转换。

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

image-20220121150127777

11.4 无序容器

tring word;
bool firstword = true; // 控制是否打印空格
while (stream >> word)
{
if (firstword)
firstword = false;
else
cout << " "; // 在单词间打印出一个空格
// transform 返回它的第一个参数或其转换之后的形式
cout << transform(word, trans_map); //打印输出
}
cout << endl; // 完成一行的转换
}
}


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

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

练习 11.35:在 buildMap 中,如果进行如下改写,会有什么效果?

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


<span style="background:#eef0f4;">当 map 中没有给定关键字时,insert 操作与下标操作+赋值操作的效果类似,都是将关键字和值的 pair 添加到 map 中。</span>

<span style="background:#eef0f4;">但当 map 中已有给定关键字,也就是新的转换规则与上一条已有规则要转换同一个单词时,两者的行为是不同的。</span><span style="background:#d4e9d5;">下标操作会获得具有该关键字的元素(也就是已有规则)的值,并将新读入的值赋予它,也就是用心读入的规则覆盖了容器中的已有规则。</span><span style="background:#dad5e9;">但 insert 操作遇到关键字已存在的情况,则不会改变容器内容,而是返回一个值指出插入失败。</span><span style="background:#eef0f4;">因此,当规则文件中存在多条规则转换相同单词时,下标+赋值的版本最终会用最后一条规则进行文本转换,而 insert 版本则会用第一条规则进行文本转换。</span>

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

[外链图片转存中...(img-2s7XEuJ5-1642757185254)]

## 11.4 无序容器

标签:11,容器,word,map,元素,关联,关键字,pair
来源: https://blog.csdn.net/kllo__/article/details/122626008

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有