关联容器共有两大类,一种是map,另一种是set。
map是关键字-值对的集合,map也称做 关联数组 ,只是和数组不一样的是数组的下标是整数,而map的下标是关键字,不一定是整数。
set是关键字的简单集合,可以理解为是一种只有关键字而没有值的map。
注意,map整个系列(map和multimap等)的头文件就是map,set整个系列(set和multiset等)头文件是set。
void Q11_03() {
string path("last_kmsg");
fstream fin(path, std::fstream::in);
map<string, size_t> words;
set<string> myset = { "<6>[", "<5>[", "<3>[", "<7>[", "<14>[", "<12>[" };
string word;
while (fin >> word) {
if (myset.find(word) == myset.end()) ++words[word];
}
for (const auto i : words) {
cout << i.first << "occurs" << i.second << endl;
}
fin.close();
}
以上例子实现了从文件中读取文本并统计各单词出现的次数,具体是从文件中先读取文本进入文件流,然后通过myset进行筛选出一些不用记录的单词,最后将单词本身作为关键字,出现的次数为其对应的值存储进words这个map中,然后通过遍历打印。
//默认初始化空容器
map<string, int> mymap;
set<string> myset;
//列表初始化
map<string, int> mymap1{
{"new",2},
{"world",3}
};
set<string> myset1{ "new","world" };
map<string, int> mymap1{
{"new",2},
{"world",3}
};
set<string> myset1{ "new","world" };
//拷贝初始化
map<string, int> mymap2(mymap1);
set<string> myset2(myset1);
这个区别很简单,map里的关键字不允许重复,而multimap里的关键字允许重复,set和multiset的区别一样。
对于map来说,关键字类型就是关键字的类型(你搁这搁这呢)举个例子,比如map
有些时候,我们使用map的关键字不一定会使用官方定义的基本类型,可能会用到我们自定义的类,这时候想要使用map,就会对关键字产生要求,需要使用关键字类型的比较函数。使用关键字的比较函数需要两步
首先需要在map和set的构造时的尖括号里给出比较操作类型,也就是比较函数的函数指针,然后在声明语句中指出需要初始化时调用的比较函数即可,如下所示
class Student {
public:
int id;
string name;
Student(): id(0),name(""){}
Student(int sid,string sname):id(sid),name(sname){}
};
//比较函数,通过id进行比较,id小的在前面,大的在后面
bool sortid(const Student& s1, const Student& s2) {
return s1.id < s2.id;
}
void set_arg_test() {
auto s1 = Student(1, "zhangsan");
auto s2 = Student(3, "lisi");
auto s3 = Student(5, "wangpo");
//decltype(sortid)*指定比较操作类型是一个函数指针,sortid指定使用该函数在初始化时排序
std::set<Student, decltype(sortid)*> class197(sortid);
class197 = { s3,s2,s1 };
//按id大小输出,结果顺序是1、3、5
for (auto s : class197)cout << "studentid->" << s.id << endl;
}
void map_arg_test() {
auto s1 = Student(1, "zhangsan");
auto s2 = Student(3, "lisi");
auto s3 = Student(5, "wangpo");
//decltype(sortid)*指定比较操作类型是一个函数指针,sortid指定使用该函数在初始化时排序
std::map<Student, int, decltype(sortid)*> class197(sortid);
class197 = {
{s1,1},
{s2,2},
{s3,3}
};
//按id大小输出,结果顺序是1、3、5
for (auto s : class197)cout << "studentid->" << s.first.id << endl;
}
pair类型是标准库定义的类型,该类型对于我们后面学习map时很重要,需要提前了解一下。
使用pair前,需要先包含头文件utility
一个pair保存两个数据类型,类似容器,它的定义如下:
pair<T1,T2> p
void pair_test() {
//默认初始化,将会对其中的类型值初始化
pair<string, int> p1;
//初始化并且赋值
pair<int, string> p2(1, "zhangsan");
pair<int, string> p3 = { 3,"lisi" };
//使用make_pair初始化,返回一个使用T1,T2初始化的pair
//pair的类型编译器根据T1和T2确认
auto p4 = std::make_pair(1, "wangwu");
}
pair具有两个属性first和second分别对应定义中的T1和T2,且这两个属性是public属性,可以直接访问。pair可以使用"<=、>=、>、<、==、!=“等操作符号,当然这些操作符号都是基于”<“和”=="实现的。
关联容器的操作其实就是赋值,添加元素,删除元素这些基本操作,下面一一介绍
key_type
容器的关键字类型。
mapped_type
只有map才有的,表示关键字-值对里的值。
value_type
value_type分两种情况:对于map来说,value_type是一个pair,该pair的关键字-值和map的关键字-值一样。对于set来说,由于只有关键字,所以value_type就是容器关键字类型,也就是key_type。
map<string,int>::key_type v1; //v1是一个string类型
map<string,int>::mapped_type v2; //v2是一个int类型
map<string,int>::value_type v3; //v3是一个pair类型
set<string>::key_type s1; //s1是一个string类型
set<string>::value_type s2; //s2是一个string类型
上面那3个类型有什么用呢,到这里就知道啦,我们平常可以在顺序容器里使用迭代器,在关联容器里也是可以使用的呀,只是和顺序容器有些不同,顺序容器的迭代器解引用后得到的是元素类型,而关联容器解引用后得到的是value_type,请牢记这一点。
关联容器的迭代器解引用后,set的value_type是const类型的,而map的迭代器解引用后,得到的value_type里,关键字key_type也是const类型的,但是我们可以改变mapped_type的值,这是允许的。
最后需要注意的是关联容器的迭代器是双向迭代器,支持自加(++)和自减(–)。
向map添加元素的4种方法
string word("hello");
map<string ,int> words;
//第1种方法
words.insert({word,1});
//第2种方法
words.insert(make_pair(word,1));
//第3种方法
words.insert(pair<string,int>(word,1));
//第4种方法
words.insert(map<string,int>::value_type(word,1));
//使用emplace的例子
words.emplace(word, 1);
insert(或者emplace)的返回值
知道了添加元素的方法,我们也要知道容器有没有添加成功呀,毕竟map和set是不允许添加key_type相同的元素的,那么这个时候就要用到insert()的返回值了,insert()的返回值是一个pair,其first成员是一个迭代器,指向具有给定关键字的元素(value_type),second成员是一个bool变量,表示是否添加成功。
注意:由于map和set的key_type不重复特性,push_back()或者push_front()在关联容器中没有意义,所以在关联容器中是无法使用与位置有关的顺序容器里的函数
//以下函数实现从文件中读取所有的单词,并统计重复单词出现的次数并存入map
void Q11_20() {
string path("last_kmsg");
fstream fin(path, fstream::in);
map<string, int> words;
string word;
if (!fin.is_open()) cout << "Open failed!\n";
while (fin >> word) {
//这里写为auto比较方便
std::pair<map<string,int>::iterator,bool> ret = words.insert({ word,1 });
if (!ret.second) {
++((ret.first)->second);
}
}
for (auto i : words) cout << i.first << "-》" << i.second << endl;
fin.close();
}
有一个有意思的点是当我们向一个multimap或者multiset里添加元素时,由于它们可以重复添加,所以就没有必要返回一个bool来告诉我们是否添加成功啦,这时insert()会返回一个指向新元素的迭代器,而不是pair啦!
关联容器的删除和顺序容器类似,我们可以通过传值一个迭代器或者一个迭代器对给erase()函数来删掉一个元素或者一个元素范围。当然关联容器里也有一个独有的erase()函数,可以通过传值key_type来删掉指定的元素,erase()函数返回删除元素的数量。
void test_erase() {
map<string, int> words{
{"Windows",1},
{"Android",2},
{"Harmony",3}
};
cout << "before erase...\n";
for (auto i : words) cout << "i.first=" << i.first << " i.second=" << i.second << endl;
//删除"Android"
words.erase(words.begin());
//删除"Android"和"Harmony"
words.erase(words.begin(), --words.end());
//删除"Android"
words.erase("Android");
cout << "after erase...\n";
for (auto i : words) cout << "i.first=" << i.first << " i.second=" << i.second << endl;
}
map具有一个像数组一样的下标操作,但是和数组又有明显的区别,首先,map的下标是关键字,数组的下标是整数,其次,map的下标关键字如果在map里不存在,那么map将会创建一个该关键字的新条目,其映射的值将使用值初始化,这一点也是最大的关键,可以看到在开头的"简单使用map和set"小节里的示例中就使用了这一特点来生成map中原本不存在的单词条目。
当我们使用map的下标时,会返回一个下标关键字映射的值,也就是mapped_type。
除了使用下标运算符访问之外,还有一种使用at(k)访问mapped_type的方法,其中k是key_type元素类型,使用该方法访问,如果下标不在map中,将不会创建新的条目,而是会报出一个"out_of_range"的异常。
下面给出一个下标访问mapped_type元素类型的例子
class Student {
public:
int id;
string name;
Student(): id(0),name(""){}
Student(int sid,string sname):id(sid),name(sname){}
};
bool sortid(const Student& s1, const Student& s2) {
return s1.id < s2.id;
}
void Q11_26() {
map<Student, string, decltype(sortid)*> stu_map(sortid);
auto s1 = Student(1, "li");
auto s2 = Student(5, "wang");
auto s3 = Student(3, "yang");
stu_map = {
{s1,"have a ticket"},
{s2,"have a apple"},
{s3,"have a bread"}
};
//使用下标是s1访问对应的mapped_type元素,这里会输出"s1->have a ticket"
cout << "s1->" << stu_map[s1] << endl;
//使用at()访问对应的mapped_type元素,这里会输出"s1->have a ticket"
cout << "s2->" << stu_map.at(s2) << endl;
}
访问关联容器元素通常使用下列5种方法
使用下标运算符会在map里没有该元素的情况下加入一个元素,而使用at()时则会报一个异常,但有时我们只想要访问元素既不想生成一个新元素也不想出现异常,这个时候怎么办呢,我们可以使用find()和count().
find(k)接收一个key_type元素的值,然后会在map中寻找和该值匹配的关键字,并返回一个迭代器,如果找到该关键字,返回一个指向给定关键字的迭代器,否则返回尾后迭代器。在multimap和multiset中返回的是第一个指向给定关键字的迭代器。
记得二者返回的迭代器解引用得到的value_type元素不同,map的迭代器解引用是一个pair,set是key_type元素
count(k)接收一个key_type元素的值,然后会在map里寻找和该值匹配的关键字,和find()返回迭代器不同的是,count返回的是该关键字在关联容器里出现的次数,map和set里返回0或1,在multimap或multiset返回值可能大于1
lower_bound(k)接收一个key_type元素,查找map或set里第一个不小于k的元素,返回第一个不小于k的迭代器。如果该关键字不在map里,lower_bound(k)会指向begin()或者end()。
upper_bound(k)接收一个key_type元素,查找map或set里第一个大于k的元素,返回第一个大于k的迭代器。和lower_bound()一样,如果该关键字不在map里,upper_bound(k)也会指向begin()或者end()。
equal_range(k)接收一个key_type元素,查找map或set里等于k的元素范围,返回一个pair,该pair存储两个迭代器,第一个迭代器指向第一个匹配该关键字的元素,第二个迭代器指向最后一个匹配元素之后的位置,如果没找到,则两个迭代器相等。
void test_map_access() {
map<int, string> words{
{1,"Android"},
{7,"Windows"},
{9,"Harmony"},
{6,"Linux"}
};
map<int, string>::iterator iter = words.find(9);
auto low_iter = words.lower_bound(0);
if (low_iter == words.end()) {
cout << "this is end\n";
}
else {
cout << low_iter->second << endl;
}
auto up_iter = words.upper_bound(9);
if (up_iter == words.end()) {
cout << "this is end\n";
}
else {
cout << up_iter->second << endl;
}
if (iter != words.end()) {
cout << iter->second << endl;
cout << words.count(10) << endl;
}
}
下面列出一个单词转换程序,从src.txt里读出需要转换的序列,再从pwd.txt里读出转换规则的序列,最后将src的序列转换输出至控制台,该例子详解可查阅《PrimerC++》第5版第11章。
/**
*
* src.txt的内容
* where r u
* y dont u send me a pic
* k thk 18r
*
**/
/**
*
* pwd.txt的内容
* brb be right back
* k okay?
* y why
* r are
* u you
* pic picture
* thk thanks!
* 18r later
*
**/
/**
*
* 输出结果:
* where are you
* why dont you send me a picture
* okay? thanks! later
*
**/
//读取转换规则的pwd.txt
void read_pwd_file(map<string, string>& mpwd) {
fstream fpwd("pwd.txt");
string line;
if (!fpwd.is_open()) {
cout << "Open failed!\n";
return;
}
while (getline(fpwd, line)) {
auto iter = std::find(line.begin(),line.end(),' ');
string key = string(line.begin(), iter);
string value = string(++iter, line.end());
mpwd.insert(std::make_pair(key, value));
}
fpwd.close();
return;
}
//读取需要转换的源文件src.txt
void read_src_file(vector<string>& src) {
fstream fsrc("src.txt");
string line;
string word;
auto str_insert = std::back_inserter(src);
if (!fsrc.is_open()) {
cout << "Open failed!\n";
return;
}
while (getline(fsrc, line)) {
istringstream isstream(line);
while(isstream>>word)str_insert = word;
str_insert = "\n";
}
fsrc.close();
return;
}
//转换的控制逻辑
void decrypt() {
map<string, string> mpwd;
vector<string> src;
read_src_file(src);
read_pwd_file(mpwd);
for (auto& word : src) {
auto iter = mpwd.find(word);
if (iter != mpwd.end()) {
word = iter->second;
}
}
for (auto i : src) {
if (i != "\n") {
cout << i << " ";
}
else {
cout << i;
}
}
}