关联容器总结

关联容器

一. 使用关联容器

1. 关联容器概述

关联容器共有两大类,一种是map,另一种是set。
map是关键字-值对的集合,map也称做 关联数组 ,只是和数组不一样的是数组的下标是整数,而map的下标是关键字,不一定是整数。
set是关键字的简单集合,可以理解为是一种只有关键字而没有值的map。

注意,map整个系列(map和multimap等)的头文件就是map,set整个系列(set和multiset等)头文件是set。

2. 简单使用map和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中,然后通过遍历打印。

3.定义关联容器

默认初始化
//默认初始化空容器
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);

4. multimap和map的区别

这个区别很简单,map里的关键字不允许重复,而multimap里的关键字允许重复,set和multiset的区别一样。

5. 关键字类型及其要求

什么是关键字类型

对于map来说,关键字类型就是关键字的类型(你搁这搁这呢)举个例子,比如map mymap里,mymap的关键字类型就是string,而set的关键字类型就是元素类型,因为set没有值,只有关键字。

关键字类型的要求

有些时候,我们使用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;

}

6. pair类型

pair类型是标准库定义的类型,该类型对于我们后面学习map时很重要,需要提前了解一下。

使用pair前,需要先包含头文件utility

pair的定义

一个pair保存两个数据类型,类似容器,它的定义如下:

pair<T1,T2> p
pair的初始化
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的相关操作

pair具有两个属性first和second分别对应定义中的T1和T2,且这两个属性是public属性,可以直接访问。pair可以使用"<=、>=、>、<、==、!=“等操作符号,当然这些操作符号都是基于”<“和”=="实现的。

二. 关联容器操作

关联容器的操作其实就是赋值,添加元素,删除元素这些基本操作,下面一一介绍

1. 关联容器的3个类型别名

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类型

2. 关联容器迭代器

上面那3个类型有什么用呢,到这里就知道啦,我们平常可以在顺序容器里使用迭代器,在关联容器里也是可以使用的呀,只是和顺序容器有些不同,顺序容器的迭代器解引用后得到的是元素类型,而关联容器解引用后得到的是value_type,请牢记这一点。

关联容器的迭代器解引用后,set的value_type是const类型的,而map的迭代器解引用后,得到的value_type里,关键字key_type也是const类型的,但是我们可以改变mapped_type的值,这是允许的。

最后需要注意的是关联容器的迭代器是双向迭代器,支持自加(++)和自减(–)。

3. 添加元素

向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啦!

4. 删除元素

关联容器的删除和顺序容器类似,我们可以通过传值一个迭代器或者一个迭代器对给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;
}

5. map的下标操作

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;

}

6. 访问元素

访问关联容器元素通常使用下列5种方法

find()和count()

使用下标运算符会在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()和upper_bound()

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)

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;
		}
	}

}

你可能感兴趣的:(c++)