《C++Primer》第十章-关联容器-学习笔记(2)

《C++Primer》第十章-关联容器-学习笔记(1)

日志:
1,2020-03-15 笔者提交文章的初版V1.0

作者按:
最近在学习C++ primer,初步打算把所学的记录下来。

传送门/推广
《C++Primer》第二章-变量和基本类型-学习笔记(1)
《C++Primer》第三章-标准库类型-学习笔记(1)
《C++Primer》第八章-标准 IO 库-学习笔记(1)
《C++Primer》第十二章-类-学习笔记(1)

set 类型

map 容器是键-值对的集合,好比以人名为键的地址和电话号码。相反地,set 容器只是单纯的键的集合。例如,某公司可能定义了一个名为 bad_checks的 set 容器,用于记录曾经给本公司发空头支票的客户。当只想知道一个值是否存在时,使用 set 容器是最适合的。例如,在接收一张支票前,该公司可能想查询 bad_checks 对象,看看该客户的名字是否存在。
除了两种例外情况,set 容器支持大部分的 map 操作,包括下面几种:
•之前列出的所有通用的容器操作。
• 表 10.3 描述的构造函数。
• 表 10.5 描述的 insert 操作。
• 表 10.6 描述的 count 和 find 操作。
• 表 10.7 描述的 erase 操作。
两种例外包括:set 不支持下标操作符,而且没有定义 mapped_type 类型。在 set 容器中,value_type 不是 pair 类型,而是与 key_type 相同的类型。它们指的都是 set 中存储的元素类型。这一差别也体现了 set 存储的元素仅仅是键,而没有所关联的值。与 map 一样,set 容器存储的键也必须唯一,而且不能修改。//和数学上的集合定义一样

set 容器的定义和使用

为了使用 set 容器,必须包含 set 头文件。set 支持的操作基本上与 map提供的相同。
与 map 容器一样,set 容器的每个键都只能对应一个元素。以一段范围的元素初始化 set 对象,或在 set 对象中插入一组元素时,对于每个键,事实上都只添加了一个元素:

// define a vector with 20 elements, holding two copies of each number from 0 to 9
vector<int> ivec;
for (vector<int>::size_type i = 0; i != 10; ++i) {
	ivec.push_back(i);
	ivec.push_back(i); // duplicate copies of each number
}
// iset holds unique elements from ivec
set<int> iset(ivec.begin(), ivec.end());  //复制
cout << ivec.size() << endl; // prints 20
cout << iset.size() << endl; // prints 10   无重复的

首先创建了一个名为 ivec 的 int 型 vector 容器,存储 20 个元素:0-9(包括 9)中每个整数都出现了两次。然后用 ivec 中所有的元素初始化一个int 型的 set 容器。则这个 set 容器仅有 10 个元素:ivec 中不相同的各个元素。

在 set 中添加元素

可使用 insert 操作在 set 中添加元素:

set<string> set1; // empty set
set1.insert("the"); // set1 now has one element
set1.insert("and"); // set1 now has two elements

另一种用法是,调用 insert 函数时,提供一对迭代器实参,插入其标记范围内所有的元素。该版本的 insert 函数类似于形参为一对迭代器的构造函数——对于一个键,仅插入一个元素:

set<int> iset2; // empty set
iset2.insert(ivec.begin(), ivec.end()); // iset2 has 10 elements

与 map 容器的操作一样,带有一个键参数的 insert 版本返回 pair 类型对象,包含一个迭代器和一个 bool 值,迭代器指向拥有该键的元素,而 bool 值表明是否添加了元素。使用迭代器对的 insert 版本返回 void 类型。

扫描二维码关注公众号,回复: 9908693 查看本文章

从 set 中获取元素

set 容器不提供下标操作符。为了通过键从 set 中获取元素,可使用find运算。如果只需简单地判断某个元素是否存在,同样可以使用 count 运算,返回 set 中该键对应的元素个数。当然,对于 set 容器,count 的返回值只能是1(该元素存在)或 0(该元素不存在):

iset.find(1) // returns iterator that refers to the element with key == 1
iset.find(11) // returns iterator == iset.end()
iset.count(1) // returns 1
iset.count(11) // returns 0

正如不能修改 map 中元素的键部分一样,set 中的键也为 const在获得指向 set 中某元素的迭代器后,只能对其做读操作,而不能做写操作

// set_it refers to the element with key == 1
set<int>::iterator set_it = iset.find(1);
*set_it = 11; // error: keys in a set are read-only 不能写set中的元素
cout << *set_it << endl; // ok: can read the key

创建“单词排除”集

第 10.3.7 节的程序从 map 对象 word_count 中删除一个指定的单词。可将这个操作扩展为删除指定文件中所有的单词(即该文件记录的是排除集)。也即,我们的单词统计程序只对那些不在排除集中的单词进行统计。使用 set 和map 容器,可以简单而直接地实现该功能:

void restricted_wc(ifstream &remove_file,map<string, int> &word_count)
{
	set<string> excluded; // set to hold words we'll ignore string remove_word;
	while (remove_file >> remove_word)  
	excluded.insert(remove_word); //输入要排除的单词列表到set集合中
// read input and keep a count for words that aren't in the exclusion setstring word;
	while (cin >> word)
// increment counter only if the word is not in excluded
	if (!excluded.count(word))  //排除集内的单词不计入
		++word_count[word]; //map容器计数
}

这个程序类似第 10.3.4 节的单词统计程序。其差别在于不需要费力地统计常见的单词。
该函数首先读取传递进来的文件,该文件列出了所有被排除的单词。读入这些单词并存储在一个名为 excluded 的 set 容器中。第一个 while 循环完成捍,该 set 对象包含了输入文件中的所有单词。

接下来的程序类似原来的单词统计程序。关键的区别在于:在统计每个单词之前,先检查该单词是否出现在排除集中。第二个 while 循环里的 if 语句实现了该功能:

// increment counter only if the word is not in excluded
if (!excluded.count(word))

如果该单词出现在排除集 excluded 中,则调用 count 将返回 1,否则返回 0。对 count 的返回值做“非”运算,则当该 word 不在 excluded 中时,条件测试成功,此时修改该单词在 map 中对应的值。与单词统计程序原来的版本一样,需要使用下标操作符的性质:如果某键尚未在map 容器中出现,则将该元素插入容器。所以下面的语句的效果是:如果 word 还没出现过,则将它插入到 word_count 中,并在插入元素后,将它关联的值初始化为 0。然后不管是否插入了新元素,相应元素的值都加 1。

++word_count[word];

multimap 和 multiset 类型

map 和 set 容器中,一个键只能对应一个实例。而 multisetmultimap类型则允许一个键对应多个实例。 例如,在电话簿中,每个人可能有单独的电话号码列表(一个人多个电话)。在作者的文章集中,每位作者可能有单独的文章标题列表(一个作者多篇文章)。multimap和 multiset 类型与相应的单元素版本具有相同的头文件定义:分别是 map 和set 头文件
multimap 和 multiset 所支持的操作分别与 map 和 set 的操作相同,只有一个例外:multimap 不支持下标运算 不能对 multimap 对象使用下标操作,因为在这类容器中,某个键可能对应多个值。为了顺应一个键可以对应多个值这一性质,map 和 multimap,或 set 和 multiset 中相同的操作都以不同的方式做出了一定的修改。在使用 multimap 或 multiset 时,对于某个键,必须做好处理多个值的准备,而非只有单一的值。

元素的添加和删除

表 10.5 描述的 insert 操作和表 10.7 描述的 erase 操作同样适用于multimap 以及 multiset 容器,实现元素的添加和删除。
由于键不要求是唯一的,因此每次调用 insert 总会添加一个元素。例如,可如下定义一个 multimap 容器对象将作者映射到他们所写的书的书名上。这样的映射可为一个作者存储多个条目

// adds first element with key Barth
authors.insert(make_pair(string("Barth, John"),string("Sot-Weed Factor")));
// ok: adds second element with key Barth
authors.insert(make_pair(string("Barth, John"),string("Lost in the Funhouse")));

带有一个键参数的 erase 版本将删除拥有该键的所有元素,并返回删除元素的个数。而带有一个或一对迭代器参数的版本只删除指定的元素,并返回 void类型

multimap<string, string> authors;
string search_item("Kazuo Ishiguro");
// erase all elements with this key; returns number of elements removed
multimap<string, string>::size_type cnt = authors.erase(search_item);   
//带有一个键参数的 erase 版本将删除拥有该键的所有元素,并返回删除元素的个数

在 multimap 和 multiset 中查找元素

注意到,关联容器 map 和 set 的元素是按顺序存储的。而 multimap 和multset 也一样。因此,在 multimap 和 multiset 容器中,如果某个键对应多个实例,则这些实例在容器中将相邻存放
迭代遍历 multimap 或 multiset 容器时,可保证依次返回特定键所关联的所有元素。
在 map 或 set 容器中查找一个元素很简单——该元素要么在要么不在容器中。但对于 multimap 或 multiset,该过程就复杂多了:某键对应的元素可能出现多次。例如,假设有作者与书名的映射,我们可能希望找到并输出某个作者写的所有书的书名。
事实证明,上述问题可用三种策略解决。而且三种策略都基于一个事实——在 multimap 中,同一个键所关联的元素必然相邻存放
首先介绍第一种策略:仅使用前面介绍过的函数。但这种方法要编写比较多的代码,所以我们将继续探索更简洁的方法。

使用 find 和 count 操作

使用 find 和 count 可有效地解决刚才的问题(用count统计次数,用find找到迭代器)count 函数求出某键出现的次数,而 find 操作则返回一个迭代器,指向第一个拥有正在查找的键的实例:

// author we'll look for
string search_item("Alain de Botton");
// how many entries are there for this author
typedef multimap<string, string>::size_type sz_type;
sz_type entries = authors.count(search_item);
// get iterator to the first entry for this author
multimap<string,string>::iterator iter =authors.find(search_item);
// find 操作则返回一个迭代器,指向第一个拥有正在查找的键的实例
// loop through the number of entries there are for this author
for (sz_type cnt = 0; cnt != entries; ++cnt, ++iter) 
	cout <<iter->second << endl; // print each title	

首先,调用 count 确定某作者所写的书籍数目,然后调用 find 获得指向第一个该键所关联的元素的迭代器。for 循环迭代的次数依赖于 count 返回的值。在特殊情况下,如果 count 返回 0 值,则该循环永不执行。

与众不同的面向迭代器的解决方案

另一个更优雅简洁的方法是使用两个未曾见过的关联容器的操作:lower_boundupper_bound。(翻译就是下界和上界)表 10.8 列出的这些操作适用于所有的关联容器,也可用于普通的 map 和 set 容器,但更常用于 ultimap 和 multiset。所有这些操作都需要传递一个键,并返回一个迭代器
表 10.8. 返回迭代器的关联容器操作:

关联容器操作 作用
m.lower_bound(k) 返回一个迭代器,指向键不小于 k 的第一个元素(大于或等于k)
m.upper_bound(k) 返回一个迭代器,指向键大于 k 的第一个元素
m.equal_range(k) 返回一个迭代器的 pair 对象。它的 first 成员等价于 m.lower_bound(k)。而 second 成员则等价于 m.upper_bound(k)

同一个键上调用 lower_bound 和 upper_bound,将产生一个迭代器范围(第 9.2.1 节),指示出该键所关联的所有元素。如果该键在容器中存在,则会获得两个不同的迭代器:lower_bound 返回的迭代器指向该键关联的第一个实例,而 upper_bound 返回的迭代器指向最后一个实例的下一位置。如果该键不在 multimap 中,这两个操作将返回同一个迭代器,指向依据元素的排列顺序该键应该插入的位置。
当然,这些操作返回的也可能是容器自身的超出末端迭代器。如果所查找的元素拥有 multimap 容器中最大的键,那么的该键上调用 upper_bound 将返回超出末端迭代器。如果所查找的键不存在,而且比 multimap 容器中所有的键都大,则 low_bound 也将返回超出末端迭代器。
lower_bound 返回的迭代器不一定指向拥有特定键的元素。如果该键不在容器中,则 lower_bound 返回在保持容器元素顺序的前提下该键应被插入的第一个位置。
使用这些操作,可如下重写程序:

// definitions of authors and search_item as above
// beg and end denote range of elements for this author
typedef multimap<string, string>::iterator authors_it;   //作者的迭代器
authors_it beg = authors.lower_bound(search_item),end = authors.upper_bound(search_item); //找出作者的迭代器,第一个和指向最后一个实例的下一个位置。
// loop through the number of entries there are for this author
while (beg != end) {
	cout << beg->second << endl; // print each title
	++beg;
}

这个程序实现的功能与前面使用 count 和 find 的程序相同,但任务的实现更直接。调用 lower_bound 定位 beg 迭代器,如果键 search_item 在容器中存在,则使 beg 指向第一个与之匹配的元素。如果容器中没有这样的元素,那么 beg 将指向第一个键比 search_item 大的元素。调用 upper_bound 设置end 迭代器,使之指向拥有该键的最后一个元素的下一位置。
这两个操作不会说明键是否存在,其关键之处在于返回值给出了迭代器范围。
若该键没有关联的元素,则 lower_bound 和 upper_bound 返回相同的迭代器:都指向同一个元素或同时指向 multimap 的超出末端位置。它们都指向在保持容器元素顺序的前提下该键应被插入的位置。
如果该键所关联的元素存在,那么 beg 将指向满足条件的元素中的第一个。
可对 beg 做自增运算遍历拥有该键的所有元素。当迭代器累加至 end 标志时,表示已遍历了所有这些元素。当 beg 等于 end 时,表示已访问所有与该键关联的元素。
假设这些迭代器标记某个范围,可使用同样的 while 循环遍历该范围。该循环执行 0 次或多次,输出指定作者所写的所有书的书名(如果有的话)。如果没有相关的元素,那么 beg 和 end 相等,循环永不执行。否则,不断累加 beg将最终到达 end,在这个过程中可输出该作者所关联的记录。

enual_range 函数

事实上,解决上述问题更直接的方法是:调用equal_range 函数来取代调用 upper_bound 和 lower_bound 函数。equal_range 函数返回存储一对迭代器的 pair 对象。如果该值存在,则 pair 对象中的第一个迭代器指向该键关联的第一个实例,第二个迭代器指向该键关联的最后一个实例的下一位置。如果找不到匹配的元素,则 pair 对象中的两个迭代器都将指向此键应该插入的位置。
使用 equal_range 函数再次修改程序:

// definitions of authors and search_item as above
// pos holds iterators that denote range of elements for this key
pair<authors_it, authors_it> pos = authors.equal_range(search_item);
// loop through the number of entries there are for this author
while (pos.first != pos.second) {
	cout << pos.first->second << endl; // print each title
	++pos.first;
}

这个程序段与前面使用 upper_bound 和 lower_bound 的程序基本上是相同的。本程序不用局部变量 beg 和 end 来记录迭代器范围,而是直接使用equal_range 返回的 pair 对象。该 pair 对象的 first 成员存储lower_bound 函数返回的迭代器,而 second 成员则记录 upper_bound 函数返回的迭代器。因此,本程序的 pos.first 等价于前一方法中的 beg,而 pos.second 等价于end。

小结

关联容器的元素按键排序和访问。关联容器支持通过键高效地查找和读取元素。键的使用,使关联容器区别于顺序容器,顺序容器的元素是根据位置访问的。

map 和 multimap 类型存储的元素是键-值对。它们使用在utility 头文件中定义的标准库 pair 类,来表示这些键-值对元素。对 map 或 multimap 迭代器进行解引用将获得 pair 类型的值。pair 对象的 first 成员是一个 const键,而 second 成员则是该键所关联的值。set 和 multiset 类型则专门用于存储键。在 map 和 set 类型中,一个键只能关联一个元素。而 multimap 和multiset 类型则允许多个元素拥有相同的键。

关联容器共享了顺序容器的许多操作。除此之外,关联容器还定义一些新操作,并对某些顺序容器同样提供的操作重新定义了其含义或返回类型,这些操作的差别体现了关联容器中键的使用。
关联容器的元素可用迭代器访问。标准库保证迭代器按照键的次序访问元素。begin 操作将获得拥有最小键的元素,对此迭代器作自增运算则可以按非降序依次访问各个元素。

发布了52 篇原创文章 · 获赞 72 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/engineerxin/article/details/104864591