一、本章学习总结
关联容器和顺序容器有根本的不同:关联容器中的元素是按关键字保存和访问的,顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的。
分类:按照三个属性:set或map; 顺序保存元素还是无序; 关键字是否可以重复; 可以分成8个类别。如unordered_multi_set是一个允许重复关键字,元素无序保存的集合。
关联容器不支持位置相关操作,如push_back, push_front, 或访问第k个元素c[k]。
set相比vector,用来保存不重复单词的好处:使用vecotr需要每次在输入单词时调用find检查单词是否已在vector中;且vector是无序线性表,find查找只能采用顺序查找的方式,其耗时与vec.size()成线性关系。set使用红黑树实现,其花费时间与vec.size()成对数关系。当单词量很大时set优势巨大。
关联容器迭代器的解引用为该容器对应
value_type
的引用。其第一个成员对应关键字,类型必定是const的。第二个成员为关键字对应的值。与vector和string不同,map的下标运算符返回的类型(mapped_type)与解引用map迭代器得到的类型(value_type)不同。
二、各小节知识点回顾
11.1 定义关联容器
关联容器的定义感觉用的比较多的有三种套路:①直接列表初始化,即大括号初始化。②构建pair类型,然后逐一insert到map中。③实际使用时先定义空map,随后根据需要使用下标访问添加元素。
- 使用举例:一个初始化map的例子:
typedef pair mypair;
map m1 = {{"word1", 1},{"word2",2},{"word3",3}};
map m2;
m2.insert(make_pair("map2_word1",1)); //使用make_pair可以让编译器根据v1和v2的类型推断出pair的类型
m2.insert(make_pair("map2_word2",2));
m2.insert(mypair("map2_word3", 43));
cout<<"map 1: "<
运行结果
$ ../../compile.sh map_defination.cpp && ./map_defination
only one arg passed.
map 1:
word1==>1
word2==>2
word3==>3
map 2:
map2_word1==>1
map2_word2==>2
map2_word3==>43
· 涉及知识点
I. pair
: 类似容器,pair是一个用来生成特定类型的模板。一个特别之处在于pair的数据成员first
和``是public的,可以直接访问。pair的使用是非常灵活的。比如下面例子,一个需要返回pair的函数:
process(vector &v)
{
if (!v.empty())
return {v.back(), v.back().size()};
//return make_pair(v.back(), v.back().size());
else
return pair(); //隐式构造返回值
}
11.2 使用关联容器
① 初步认识map和set
map
即关联数组,与普通数组的区别在于下标不必是整数。set则仅仅是关键字的简单集合。当想知道一个值是否存在时,set最适用。
顺便回顾下顺序容器:若元素很小(如int)且大致数量事先可知,在程序运行中不会剧烈变化,大部分情况只需要在末尾添加或删除,且需要频繁访问任意位置的元素,则vector
可带来最高效率;若需要频繁在头部和胃部添加删除元素,则deque最好;若元素较大(如大的类对象),数量预先不知道,或是程序运行过程中频繁变化,对元素的访问更多的是顺序访问全部或者很多元素,则list很合适。
- 使用举例:单词计数程序
功能:记录给定输入单词中各个单词出现次数。注意去除标点符号和一些无意义的词如the, a等; 忽略大小写。
#include
#include
#include
运行结果:
$ ../../compile.sh word_count.cpp && ./word_count
only one arg passed.
"1122" occurs 1 times
"Another" occurs 1 times
"above" occurs 1 times
"added" occurs 1 times
"appended" occurs 1 times
"are" occurs 1 times
"can" occurs 1 times
"chapter" occurs 1 times
"exercise" occurs 1 times
"long" occurs 1 times
"many" occurs 1 times
"more" occurs 1 times
"new" occurs 1 times
"not" occurs 1 times
"receive" occurs 1 times
"sentence" occurs 2 times
· 涉及知识点:
I. map的一个重要特点是:在使用下标运算符"[]"访问元素时,如果关键字不存在,则map会为该关键字创建一个元素并插入到map中,关联值将进行值初始化。同时map.size()将会+1. 比如map
II. 与之相关的方法是通过map.at(k)函数访问关键字为k的元素,同时进行参数检查。如果k不再map中,抛出out_of_range异常。
III. 一个有趣的现象:word_count这个关联容器在输出时,元素已经按照关键字的字典顺序排列好了。这就是所谓的有序容器。即map等有序容器在构建时对关键字类型是有潜在要求的:即关键字类型必须定义元素比较的方法。简而言之就是该关键字类型需要定义一个"<"运算符,或者一个实现"<"功能的操作(即严格弱序)。
- 使用举例:练习严格弱序:创建一个包含Sales_data类型元素的set。
#include
#include
#include
#include
#include
运行结果:
$ ../../compile.sh keyType_strict_weak_ordering.cpp && ./keyType_strict_weak_ordering
only one arg passed.
书目信息个数: 3
书目: book-1-a售价: 11.5销量: 20收入: 230
书目: book-2-b售价: 20.2销量: 30收入: 606
书目: book-3售价: 92.5销量: 40收入: 3700
- 注意:decltype获得一个函数指针类型时,必须加上一个
*
。当我们使用一个函数名时,在需要的情况下它会自动转换成一个指针。
11.3 关联容器操作
①关联容器额外的类型别名
key_type
和value_type
对于set来说都对应关键字类型;对于map来说前者对应关键字类型,后者对应一个pairmapped_type
为map所特有,代表其值的类型。
②关联容器迭代器
①中值得注意的一点是key_type前面的const。之所以这么说,是因为当解引用一个关联容器迭代器时,我们会得到一个类型为容器的value_type的值的引用。对于map而言,其value_type为一个pair。该pair的first成员保存const的关键字,second保存成员值;对set而言由于只有关键字,故set的迭代器是const的。即set的迭代器的解引用是只读的。
- 使用举例:
auto map_it = word_count.cbegin();
while(map_it != word_count.cend())
{
cout<first<<" occurs "<second<<" times"<
此外,迭代器的另一个使用场景是容器成员函数返回值时。比如insert
函数。map.insert函数的用法见p384页。对于不包含重复关键字的容器,添加单一元素的insert和emplace版本返回一个pair,其first成员是一个迭代器,指向具有给定关键字的元素(若给定关键字不存在,则该迭代器指向新插入的元素位置),second成员是一个bool,指出元素是插入成功还是已经存在于容器中。
- 使用举例:还是之前的单词计数程序,不过这里用insert实现:
while(cin>>word)
++word_count.insert({word, 0}).first->second;
//若word已存在于map中,insert返回一个指向该word位置的迭代器;
//若不存在则返回指向新插入的word对应位置的迭代器。然后对迭代器的second成员即mapped value进行增一。
③关联容器与算法
关联容器通常不使用泛型算法。比如要搜索容器时,可以用关联容器自定义的find成员函数代替泛型算法的find函数。
对于set而言,不能通过其迭代器对容器元素进行修改(添加)。即使是通过back_inserter或者inserter等试图使用插入迭代器也不行。
④multimap 和 multiset 中的元素查找
注意,这两个容器定义在各自的同名头文件中。
元素查找。直接看一个例子:打印muiltimap中某个关键字对应的所有值:
// authors 为一个 multimap类型的对象。
// 方法一:使用lower_bound和upper_bound返回一个迭代器范围
for (auto beg = authors.lower_bound(search_item), end = authors.upper_bound(search_item); beg != end; ++beg)
cout<second<second<
注意:这段代码中几个函数的返回值需要区分一下。lower_bound和upper_bound通常一起使用,来返回一个迭代器范围。当查找的元素不存在时,这两个函数返回相同的迭代器,不需要关心该迭代器具体指向哪里(可能是随便指的...)。重要的是两个迭代器指向同一个位置。equal_range
返回一个迭代器pair,若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。
三、章节练习——一个单词转换的map
3.1 功能定义:
给定一个单词转换文件以及一个待转化的文本,要求输出单词转换之后的文本。映射表如下:
brb be right back
k okay?
y why
r are
u you
pic picture
thk thanks!
l8r later
3.2 分析:
考虑到需要建立的单词映射表,该程序肯定要使用map容器。此外。注意到给定的映射表存在一个简写映射多个单词的情况。要考虑如何将第一个单词(简写)读取至map的key,剩下的多个单词读取到map的value中。
刚开始尝试这么写(错误写法一):
map rule;
string brif, comp;
while(in>>brif>>comp)
{
rule.insert(make_pair(brif, comp));
}
return rule;
但是这样写有两个问题:①显然映射文件的每行代表一条映射,这样读取输入流就丢弃了行信息;
②in>>brif>>comp
将string 两两配对。如果映射关系每行都只有两个单词的话可行。但是无法完成如"brb --> be right back" 这样的映射。
后来又这么写(错误写法二):
string line, word;
vector line_vec;
while(getline(in_map, line))
{
//处理每一行
stringstream stream(line);
while(stream>>word)
line_vec.push_back(word);
mps.insert(make_pair(line_vec[0], accumulate(line_vec.begin()+1, line_vec.end(), string(" "))));
}
这样写是希望将每行读入一个vector中,然后把vector第一个元素作为key,后面所有元素拼接起来作为value...可惜accumulate的第三个参数不是dilemma,而是初始元素...所以这样拼接之后元素都连在一起。
正确写法:对于每一行,先使用in>>word读取一个,然后再使用getline读取后面的输入流:while(in>>brif && getline(in, comp)
。 完整程序如下:
#include
#include
#include
在查代码的时候发现一段有趣的程序,使用Istringstream可以方便的对string进行split,也记录在下面:
参考:stackoverflow
#include
#include
std::string input = "abc,def,ghi";
std::istringstream ss(input);
std::string token;
while(std::getline(ss, token, ',')) {
std::cout << token << '\n';
}