《C++ Primer》读书笔记—第十一章 关联容器
声明:
- 文中内容收集整理自《C++ Primer 中文版 (第5版)》,版权归原书所有。
- 学习一门程序设计语言最好的方法就是练习编程
一、使用关联容器
1、关联容器中的元素按关键字保存和访问,顺序容器中的元素则按照它们在容器中的位置来顺序保存访问。关联容器支持高效的关键字查找和访问。
2、两个主要的关联容器是map和set。
map中的元素是一些关键字值对:关键字起索引作用,值表示与索引相关联的数据。
set中每个元素包含一个关键字;set支持高效的关键字查询操作--检查一个指定的关键字是否在set中。
3、两个主要的关联容器是map和set,细分为8种
- 有序
- 关键字不可重复
- map
- set
- 关键字可重复
- multimap
- multiset
- 关键字不可重复
- 无序
- 关键字不可重复
- unordered_map
- unordered_set
- 关键字可重复
- unordered_multimap
- unordered_multiset
- 关键字不可重复
4、使用map进行单词计数程序
1 map<string, size_t> word_count; //string到size_t的空map 2 string word; 3 while(cin >> word) 4 { 5 ++word_count[word];//提取Word的计数器并加1 6 } 7 8 for(const auto &w : word_count) 9 { 10 cout << w.first << " occurs " << w.second << "time(s)" << endl; 11 }
map中元素的关键字是string类型,值是size_t类型,当对word_count进行下标操作时,可以使用一个string作下标,获得与此string相关联的size_t类型的计数器。
5、课后11.3题,自己写单词计数程序:
1 #include<iostream> 2 #include<string> 3 #include<fstream> 4 #include<vector> 5 #include<map> 6 using namespace std; 7 8 int main(int argc, char**argv) 9 { 10 //map的定义 11 map<string,size_t> word_count; 12 fstream in("1.txt");//定义一个输入流 13 string word; 14 15 while (in>>word) 16 { 17 ++word_count[word]; 18 } 19 20 //map同样支持迭代器操作 21 map<string ,size_t>::iterator mapi; 22 for (mapi = word_count.begin(); mapi != word_count.end(); ++mapi)//C++ 11支持:const auto &s : word_count 23 { 24 //两个成员分别代表关键字和对应值 25 cout<<mapi->first<<" "; 26 cout<<mapi->second<<" "<<endl; 27 } 28 29 return 0; 30 }
6、
1 map<string, size_t> word_count; 2 set<string> exclude = {"the", "but"}; 3 4 string word; 5 6 while(cin >> word) 7 { 8 if( exclude.find(word) == exclude.end() ) //只统计不在exclude中的单词 9 { 10 ++word_count[word]; //获取并递增word的计数器 11 } 12 } 13 14 for(const auto &w : word_count) 15 { 16 cout << w.first << " occurs " << w.second << "time(s)" << endl; 17 }
二、关联容器概述
1、map既需要关键字类型又需要指明值类型。set只需要指明关键字类型,因为set没有值。每个关联容器都定义一个默认构造函数,可以创建一个指定的空容器。既可以把关联容器初始化为另一个同类型容器的拷贝,或是从一个值范围来初始化关联容器,只要这些值能转化为容器所需的类型即可。在新标准下,可以对容器值初始化。
1 set<string> exclude = {"the", "but"}; 2 map<string, string> authors = { {"joyce", "james"}, 3 {"austen","jane"}, 4 {"dickens","charies"} };
2、multimap和multiset可以有多个元素具有相同的关键字:
1 vector<int> ivec; 2 for( vector<int>::size_type i = 0; i != 10; ++i ) 3 { 4 ivec.push_back(i); 5 ivec.push_back(i); //每个数重复保存一次 6 //ivec: 0 0 1 1 2 2 ... 7 } 8 9 set<int> iset(ivec.cbegin(), ivec.cend()); 10 multiset<int> miset(ivec.cbegin(), ivec.cend()); 11 cout<<ivec.size()<<endl;//打印20 12 cout<<iset.size()<<endl; //打印10 13 cout<<miset.size()<<endl;//打印20
3、标准库使用<运算符比较两个关键字。也可以自定义一个比较操作,但必须是在关键字类型上定义一个严格弱序(strict weak ordering)。
4、对于自定义的关键字类型,我们需要提供比较函数,并且需要在定义关联容器时就指定。
从关联容器定义的构造函数也可以看出这一点。
5、pair保存两个数据成员,定义在头文件utility中。创建一个pair时,必须提供两个类型名,pair的数据成员将具有对应的类型,两个类型不一定一样。
1 pair<string,string> anon //两个string 2 pair<string, size_t> word_count; //保存一个string和一个size_t 3 pair<string, vector<int>> line; //保存string和vector<int>
可以提供初始化器:
1 pair<string,string> author{"James","Joyce"};
6、pair的数据成员是public的。分别命名为first和second。
7、对pair的返回值进行列表初始化:
1 pair<string, int> 2 process(vector<string> &v) 3 { 4 if(!v.empty()) 5 return {v.back(),v.back().size()};//列表初始化 6 else 7 return pair<string, int>();//隐式构造返回值 8 } 9 }
三、关联容器操作
1、关联容器额外的类型名
- key_type 关键字类型
- mapped_type map值类型
- value_type 对于set,与key_type相同。对于map,为pair<const key_type,mapped_type>
只有map类型才定义了mapped_type。
2、解引用关联容器的迭代器,会得到一个类型为容器的value_type的值的引用。set的关键字只能是const的。只能读,不能改。
3、遍历关联迭代器,循环打印单词计数程序:
1 //获得一个指向首元素的迭代器 2 auto map_it = word_count.cbegin(); 3 //比较当前迭代器和尾后迭代器 4 while( map_it != word_count.cend() ) 5 { 6 //解引用迭代器,打印关键字-值对 7 cout << map_it->first << " occurs " << map_it->second << " times " << endl; 8 ++map_it; //递增迭代器,移动到下一个元素 9 }
4、通常不对关联容器使用泛型算法,关键字是const这一特性意味着不能将关联容器传递给修改或重排容器元素的算法。可用于只读算法,但很多这类算法都要搜索序列。关联容器使用find成员,通过给定关键字直接获取元素。所以不使用泛型的find。
5、添加元素。insert成员可以向容器中添加一个元素或一个元素范围。
有两个版本,接收一个迭代器或者一个初始化列表。
1 vector<int> ivec = { 2, 4, 2, 4 }; 2 set<int> set2; 3 set2.insert( ivec.begin(), ivec.end() ); 4 set2.insert( { 1, 3, 1, 3 } );
6、对map进行insert操作时,必须记住元素类型是pair。
1 word_count.insert({word, 1}); 2 word_count.insert(make_pair(word,1)); 3 word_count.insert(pair<string,size_t>(word,1)); 4 word_count.insert(map<string,size_t>::value_type(word,1));
7、对于不包含重复关键字的容器,insert返回一个pair。它的first成员是一个迭代器,指向具有给定关键字的元素,second成员是一个bool值,如果关键字已经在容器中,返回一个false。
用insert重写单词计数程序:
1 map<string, size_t> word_count; 2 string word; 3 while(cin >> word) 4 { 5 //插入一个元素,若关键字等于word,则值为1 6 //若word已在word_count中,insert什么都不做 7 auto ret = word_count.insert({word, 1}); 8 if(!ret.second) 9 { 10 ++ret.first->second; 11 } 12 }
8、对允许重复关键字的容器,insert返回一个指向新元素的迭代器。无须返回一个bool值,因为总是会插入新元素。
1 multimap<string, string> authors; 2 authors.insert({"author1", "book1"}); 3 authors.insert({"author1", "book2"});
9、删除元素:
可以传递给erase一个迭代器或者迭代器对,来删除一个元素或一个元素范围。与顺序容器相类似,指定的元素被删除,函数返回void。
提供一个额外的erase操作,接受一个key_type参数,此版本删除所有匹配给定关键字的元素,返回实际删除元素的数量。可以用此版本在打印结果之前从word_count中删除一个元素。
10、map的下标操作:
仅map和unordered_map提供了下标操作
与其他下标运算符不同的是:
如果关键字不在map中,会创建一个元素插入到map中,关联值将进行值初始化
1 c[k] 若关键字不在容器中会添加 2 c.at(k) 若关键字不在容器中会抛异常
map的下标操作返回的是一个mapped_type对象。
下标运算符可能插入一个新元素,只可以对非const的map使用下标操作。
11、下标和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();
find返回一个迭代器,指向第一个关键字为k的元素,若k不存在,则返回尾后迭代器。
lower_bound和upper_bound不适用于无序容器。
12、如果一个multimap或multiset中有多个元素具有给定关键字,则这些关键字在容器中相邻存储。
13、如果想把具有同一关键字的所有元素都打印出来,最直观的方法是使用find和count。
1 string search_item("Alain"); //要查找的作者 2 auto entries = authors.count(search_item); //元素数量 3 auto iter = authors.find(search_item); //此作者的第一本书 4 //用一个循环查找此作者的所有著作 5 while(entries) 6 { 7 cout << iter->second << endl;//打印每个题目 8 ++iter; //前进到下一本书 9 --entries; //记录已经打印了多少书 10 }
14、lower_bound返回的迭代器指向第一个具有给定关键词的元素,upper_bound返回的迭代器指向最后一个匹配给定关键字之后的位置。如果元素不在multimap中,则lower_bound和upper_bound返回相等的迭代器,指向一个不影响排序的关键字插入位置。因此,使用相同的关键字调用lower_bound和upper_bound会得到一个迭代器范围,表示所有具有该元素关键字的范围。如果两者返回相同的迭代器,则表示关键字不在容器中。
1 for(auto beg = authors.lower_bound(search_item), 2 end = authors.upper_bound(search_item); 3 beg != end; ++beg) 4 { 5 cout << beg->second << endl; 6 }
15、equal_range函数:接受一个关键字,返回一个迭代器pair,若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可插入的位置。
1 //pos保存迭代器时,表示与关键字匹配的元素范围 2 for(auto pos = authors.equal_range(search_item); pos.first != pos.second; ++pos.first) 3 { 4 cout << pos.first->second << endl; 5 }
四、无序容器
1、有序容器通过比较运算符来组织元素,无序容器通过哈希函数和==运算符来组织元素。
2、可以使用无序容器替换对应的有序容器,但由于元素未按顺序存储,一个使用无序容器的程序的输出通常与有序容器不同。
用unorder_map重写最初的单词计数程序:输出单词的顺序不会按照字典排序。
unorder_map<string, size_t> word_count; //string到size_t的空map string word; while(cin >> word) { ++word_count[word];//提取Word的计数器并加1 } for(const auto &w : word_count) { cout << w.first << " occurs " << w.second << "time(s)" << endl; }
3、为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。
哈希函数:将给定类型的值映射到一个整形值的函数,相同的值必须映射到相同的整数,不同的值尽可能映射到不同的整数。
如果容器允许重复关键字,则所有具有相同关键字的元素在同一个桶中。无序容器的性能依赖于哈希函数的质量和桶的数量和大小。
如果一个桶中保存了很多元素,需要顺序搜索这些元素来查找我们需要的那个,那么查找一个特定元素就需要大量比较操作。
4、无序容器管理操作:
桶接口
- c.bucket_count() 正在使用的桶的数量
- c.max_bucket_count() 容器能容纳的最多的桶的数量
- c.bucket_size(n) 第n个桶中有多少个元素
- c.bucket(k) 关键字k的元素在哪个桶中
桶迭代
- local_iterator 可以用来访问桶中元素的迭代器类型
- const_local_iterator 桶迭代器的const版本
- c.begin(n), c.end(n) 桶n的首元素迭代器和尾后迭代器
- c.cbegin(n), c.cend(n) 与前两个函数类型,但返回const_local_iterator
哈希策略
- c.load_factor() 每个桶的平均元素数量 返回float
- c.max_load_factor()
- c.rehash(n)
- c.reserve(n) 重新存储,使得c可以保存n个元素且不必rehash
5、类似于为有序容器提供比较操作,我们需要为无序容器提供hash值计算函数和相等判断函数。
附:一个单词转换的map:
给定一个string,转化为另一个string。输入两个文件,第一个保存一些规则,用来转换第二个文件中的文本。每条规则由两个部分组成:一个可能出现在输入文件中的单词和一个用来替换他的短语。每当一个单词出现时,就把他替换成对应的短语。第二个文件包含要转换的文本。
3.20:下午2.30开会到6.30。师兄们汇报研究成果,给我们分配任务。要求暑假前出文章。亚历山大。
3.21:大四毕设要做DPC的相关,之前没做过这方面。给他整理了之前参考资料。源码也可以给他,本科毕业应该不难吧。别太烦我就行,指导反而更费时间。
P391,一个单词替换的map还没有看,很有用的一个应用。
第十一章结束,且随疾风前行。