文章目录
C++primer-学习心得-第11章-关联容器
关联容器支持高效的关键词查找和访问。
按关键字有序保存元素
- map:保存键值(key-value)对
- set:只保存关键字的容器,关键字即值
- multimap:关键字可重复出现的map
- multiset:关键字可重复出现的set
无序集合
- unordered_map:用哈希函数组织的map
- unordered_set:用哈希函数组织的set
- unordered_multimap
- unordered_multiset
11.1 使用关联容器
练习11.4
#include <iostream>
#include <map>
#include <set>
#include <string>
using namespace std;
int main()
{
map<string, size_t>wc;
string str;
bool isal = true;
while (cin >> str)
{
for(auto &c:str)
{
if (isalpha(c))
{
if (isupper(c))
c = tolower(c);
}
else
isal = false;
}
if (isal)
++wc[str];
}
for (const auto& c : wc)
cout << c.first << "-occurs-" << c.second << "-times " << endl;
}
11.2 关联容器概述
关联容器都支持前面第九章中介绍的普通容器操作,不支持顺序容器的位置相关操作(如push_back),不支持构造函数或插入操作这些接受一个元素值和一个数量值的操作。此外,关联容器支持一些顺序容器不支持的操作和类型别名,我们后面会详细介绍。
关联容器的迭代器都是双向的。
1. 定义关联容器
如
map<string,size_t>wc;
set<string> ex={"the","a","and"};
map<string,string>={{"Joyce","James"},{"Austen","Jane"}};
练习11.7
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
using namespace std;
int main()
{
map<string, vector<string>> fam;
string str, famn;
vector<string>fam_name;
cout << "add family: first name+last name" << endl;
bool addp = false;
while (cin >> famn>>str)
{
fam[famn].push_back(str);
}
for (const auto& c : fam)
{
cout << "family: " << c.first << " have members including: ";
for (const auto cc : c.second)
cout << cc << " , ";
cout << endl;
}
}
练习11.8
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
using namespace std;
int main()
{
vector<string>dic;
string str;
while(cin>>str)
{
if (find(dic.begin(), dic.end(), str) == dic.end())
dic.push_back(str);
}
for (auto c : dic)
cout << c << endl;
}
2. 关键字类型的要求
首先,对于有序容器,关键字类型必须定义元素比较的方法,(很简单的道理,你不告诉系统怎么比较大小系统怎么排序),即关键字类型要有<运算符。上一章我们知道了可以向一个算法提供自定义的比较操作(谓词和lambda表达式),类似地,我们也可以提供自定义操作来代器关键字上的<运算符。这个自定义的比较函数必须是严格弱序的,这里我们可以根据常识来大概猜到这个比较函数需要满足的性质。
我们自定函数类型是放在尖括号中元素类型之后,例如
bool comp(const Sales_data &l,const Salesdata &r){
return l.isbn()>r.isbn;
}
multiset<Sales_data,decltype(comp)*> bookstore(comp);
3.pair类型
pair定义在头文件utility中。一个pair保存两个数据成员。(pair正好可以看成一个键-值对)
- pair<T1,T2> p;
- pair<T1,T2>P(v1,v2);
- pair<T1,T2>P={v1,v2};
- make_pair(v1,v2);
- p.first,p.second
- p1 relop p2
- p1==p2
- p1!=p2
练习11.12
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <utility>
using namespace std;
int main()
{
vector<pair<string, int>> test;
vector<string>s = { "1","2","3","4","5","6","7","8","9" ,"0"};
vector<int>ss = { 1,2,3,4,5,6,7,8,9,0 };
for(int i=0;i<s.size();i++)
{
test.push_back(make_pair(s[i], ss[i]));
}
for (auto& c : test)
cout << c.first << "-->" << c.second << endl;
}
练习11.13
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <utility>
using namespace std;
int main()
{
vector<pair<string, int>> test,test1,test2;
vector<string>s = { "1","2","3","4","5","6","7","8","9" ,"0"};
vector<int>ss = { 1,2,3,4,5,6,7,8,9,0 };
for(int i=0;i<s.size();i++)
{
test.push_back(make_pair(s[i], ss[i]));
pair<string, int>p1(s[i], ss[i]);
pair<string, int>p2 = { s[i],ss[i] };
test1.push_back(p1);
test2.push_back(p2);
}
for (auto& c : test)
cout << c.first << "-->" << c.second << endl;
}
练习11.14
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
using namespace std;
int main()
{
map<string, vector<pair<string, string>>> fam;
string str, famn,birth;
vector<string>fam_name;
cout << "add family: first name+last name" << endl;
bool addp = false;
while (cin >> famn>>str>>birth)
{
fam[famn].push_back({str,birth});
}
for (const auto& c : fam)
{
cout << "family: " << c.first << " have members including: ";
for (const auto cc : c.second)
cout << cc.first<<"--->"<<cc.second << "\n ";
cout << endl;
}
}
11.3 关联容器操作
关联容器定义了以下的几个类型别名:
- key_type:容器的关键字类型
- mapped_type:关键字关联的类型,只适用map
- value_type:对于set(set键即值)来说和key_type相同,对于map,为pair<const key_type,map_type>
在map中,元素是键-值对,所以记住,每个元素都是一个pair对象,有一个关键字及其相关联的值。但注意到我们key_type前加了一个const关键字,因为map的特性是不能改变键(key),(很合理的设定,我们是要通过key来检索value,所以key是不能改变的,容器也没提供任何改变key的方法),所以要在pair的key_type前加const。(另外,别弄混了mapped_type和value_type,对容器来说,其中的元素即value,对于key来说,相关联的对象即value,我觉得c++里对这两个对象都用value这个词是容器造成误解的)
set<string>::value_type v1;//string
set<string>::key_type v2;//string
map<string,int>::key_type v3;//string
map<string,int>::mapped_type v4;//int
map<string,int>::value_type v5;//pair<const string,int>
1.关联容器迭代器
关联容器的迭代器指向的类型是容器的value_type,所以迭代器解引用得到的是一个value_type类型的引用。对于map来说,迭代器指向的就是一个pair对象,first成员保存const 键,second成员保存值。另外set的键也是const的,只能用迭代器访问set中的元素而不能修改。
利用迭代器可以轻易的遍历容器,对于关联容器也是如此。泛型算法的话,由于我们不能修改key自身的值,所以很多算法是不可用的,但那些只读算法还是可用的。但这些算法需要搜索序列,而关联容器通过key来访问value的机制使算法的使用并不理想。但关联容器自身提供了一些成员函数可以更快速的实现,如find算法。
如下面的简单的遍历map和set的程序
#include <iostream>
#include <vector>
#include <string>
#include <set>
#include <map>
using namespace std;
int main()
{
vector<string> s = { "a","b","c","d","e","f","g" };
map<string, int>m;
int i = 0;
set<string> ss;
for (auto c : s)
{
m[c] = i++;
ss.insert(ss.end(), c);
}
for (auto it = m.begin(); it != m.end(); it++)
cout << it->first << "-->" << it->second << endl;
for (auto it = ss.begin(); it != ss.end(); it++)
cout << *it << endl;
}
2.添加元素
由于map和set的key使不重复的,所以插入一个已存在的元素对容器是没有影响的。
向map添加元素一共有四种方法:
- w.insert({word,1});
- w.insert(make_pair(word,1));
- w.insert(pair<string,int>(word,1));
- w.insert(map<string,int>::value_type(word,1));
关联容器有下面的一些插入操作
- c.insert(v)
- c.emplace(args)
- c.insert(b,e)
- c.insert(i1)
- c.insert(p,v)
- c.emplace(p,args)
我们可以通过insert的返回值来知道插入是否成功。添加单一元素的insert和emplace返回一个pair,pair的first成员是一个指向给定关键字元素的迭代器,second成员是bool值,如果关键字容器中已有,则insert什么也不做,返回值的second成元为false,如果关键字是容器中没有的,元素成功插入容器,返回值的second成员为true。
如下实现对每个输入单词统计次数
#include <iostream>
#include <vector>
#include <string>
#include <set>
#include <map>
using namespace std;
int main()
{
map<string, size_t>wc;
string word;
while(cin>>word)
{
auto ret = wc.insert({ word,1 });
if (!ret.second)
++ret.first->second;//如果插入元素失败则定位相关的元素使其相关联的value递增
//等价的表述为 ++((ret.first)->second);
}
for (const auto& c : wc)
cout << c.first << "-->" << c.second << endl;
}
练习11.20
可以和练习11.4对比,就可以知道下表操作和insert操作,明显还是下标操作简单得多
#include <iostream>
#include <map>
#include <set>
#include <string>
using namespace std;
int main()
{
map<string, size_t>wc;
string str;
bool isal = true;
while (cin >> str)
{
for (auto& c : str)
{
if (isalpha(c))
{
if (isupper(c))
c = tolower(c);
}
else
isal = false;
}
if (isal)
{
++wc.insert({ str,1 }).first->second;
}
}
for (const auto& c : wc)
cout << c.first << "-occurs-" << c.second << "-times " << endl;
}
3.删除元素
- c.earse(k):从c中删除每个键为k的元素,返回size_type值表示删除的元素数量
- c.erase§:从c中删除迭代器p指向的元素,返回指向删除元素之后的元素的迭代器
- c.erase(b,e):删除迭代器b和e范围内的元素返回e
4.map的下标操作
- c[k]
- c.at(k):带参数检查,若k不在c中会抛出out-of-range异常
5. 访问元素
下面列出关联容器中查找元素常用的一些操作:
- c.find(k)
- c.cout(k)
- c.lower_bound(k):返回指向第一个键不小于k的元素的迭代器
- c.upper_bound(k):返回指向第一个键大于k的元素的迭代器
- c.equal_range(k):返回一个pair,其两个成员都为迭代器表示等于k的元素的范围,若k不存在则返回的两个值均为c.end()
注意lower_bound和upper_bound不适用于无序容器
练习11.32
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
using namespace std;
int main()
{
multimap<string, string> auth;
string name, works;
while(cin>>name>>works)
auth.insert({ name,works });
for (const auto& c : auth)
cout << c.first << "-->" << c.second << endl;
cout << "input a name to erase:" << endl;
auto a = auth.find("test");
if(a!=auth.end())
auth.erase(a);
for (const auto& c : auth)
cout << c.first << "-->" << c.second << endl;
}
6. 一个单词转换的map
最后我们利用学到的这些知识来实现一个将给定文本中的单词替换并输出。
- 第一步是创建两个文本文件放在cpp源文件同目录下:
change.txt
brb be right back
k okay
y why
r are
u you
pic picture
thk thanks!
18r lather
origin.txt
where r u
y dont u send me a pic
k thk 18r
- 将change.txt中的替换规则读取到一个map上作为替换映射。
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <fstream>
#include <iterator>
using namespace std;
int main()
{
ifstream is("change.txt");
map<string, string>change_map;
string str;
while(getline(is,str))
{
auto f = str.find(" ");
change_map[string(str,0,f)]=string(str,f+1,str.size());
}
for (const auto& c : change_map)
cout << c.first << "->" <<c.second<< endl;
}
- 这一步是难点,我们需要读取原文本,先是用getline一行一行地读取,然后读取每一行上的每个单词,每个单词是以空格相分隔的,但实际操作的时候总是读取不了最后一个单词,所以我后面把读取的每一行的字符串后面都加一个空格。利用find判断文本中的单词是否在我们的替换规则中出现,出现的话就替换。最终我先保存在vector中
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <fstream>
#include <iterator>
using namespace std;
int main()
{
ifstream is("change.txt");
map<string, string>change_map;
vector<string>content;
string str, dic, strs;;
while(getline(is,str))
{
auto f = str.find(" ");
change_map[string(str,0,f)]=string(str,f+1,str.size());
}
is.close();
ifstream in("origin.txt");
while(getline(in,str))
{
strs = "";
cout << str << endl;
str += " ";
auto f = str.find(" ");
for(auto c:str)
{
if(isspace(c))
{
cout << dic << endl;
auto re = change_map.find(dic);
if(re!=change_map.end())
strs = strs.empty() ? change_map[dic] :strs+ " " + change_map[dic];
else
strs = strs.empty() ? dic :strs+ " " + dic;
dic = "";
}else
dic += c;
}
content.push_back(strs);
}
for (const auto& c : change_map)
cout << c.first << "->" <<c.second<< endl;
for (auto c : content)
cout << c << endl;
}
- 通过上一步运行发现没有问题,就可以把替换后的文本写入新文件了。
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <vector>
#include <fstream>
#include <iterator>
using namespace std;
int main()
{
ifstream is("change.txt");
map<string, string>change_map;
vector<string>content;
string str, dic, strs;;
while(getline(is,str))
{
auto f = str.find(" ");
change_map[string(str,0,f)]=string(str,f+1,str.size());
}
is.close();
ifstream in("origin.txt");
while(getline(in,str))
{
strs = "";
cout << str << endl;
str += " ";
auto f = str.find(" ");
for(auto c:str)
{
if(isspace(c))
{
cout << dic << endl;
auto re = change_map.find(dic);
if(re!=change_map.end())
strs = strs.empty() ? change_map[dic] :strs+ " " + change_map[dic];
else
strs = strs.empty() ? dic :strs+ " " + dic;
dic = "";
}else
dic += c;
}
content.push_back(strs);
}
for (const auto& c : change_map)
cout << c.first << "->" <<c.second<< endl;
for (auto c : content)
cout << c << endl;
ofstream out("new.txt");
ostream_iterator<string>o(out, "\n");
for (auto c : content)
*o = c;
}
11.4 无序容器
无序容器不是使用比较运算符来组织元素,而是使用一个哈希函数(hash function)和关键字类型的==运算符。哈希函数,在Java里面是个比较重要的概念。
理论上哈希函数能获得更好的平均性能,但需要一些性能测试和调优工作。
无序函数在存储上组织为一组桶。下面列出一些无序容器的操作。
桶接口
- c.bucket_count():正在使用的桶的数量
- c.max_buket_count():容器能容纳的最多的桶的数量
- c.bucket_size(n):第n个桶中有多少个元素
- c.bucket(k):关键字为k的元素在哪个桶中
桶迭代
- local_iterator:用来访问桶中元素的迭代器类型
- const_local_iterator:const版本
- c.begin(n),c.end(n):迭代器
- c.cbegin(n),c.cend(n):const版本
哈希策略
- c.local_factor():每个桶的平均元素数量,返回float
- c.max_load_factor():c试图维护的平均桶大小
- c.rehash(n):重组存储使bucket_count>=n且bucket_count>size/max_load_factor
- c.reserve(n):重组存储使c可以保存n个元素而不必rehash
通常,使用无序容器可以直接替换使用有序容器的版本,只是顺序上存在差别
如输入单词计数
#include <iostream>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
using namespace std;
int main()
{
unordered_map<string, int>wc;
string str;
bool isal = true;
while (cin >> str)
{
for (auto& c : str)
{
if (isalpha(c))
{
if (isupper(c))
c = tolower(c);
}
else
isal = false;
}
if (isal)
++wc[str];
}
for (const auto& c : wc)
cout << c.first << "-occurs-" << c.second << "-times " << endl;
}