C++进阶 -- set、map、multiset、multimap的介绍及使用
一、关联式容器
序列式容器
我们已经接触过STL中的部分容器,比如:vector、list、 deque、forward_list(C++11)等,这些容器统称为序列式容器。因为其底层为线性序列的数据结构存储的是元素本身。
关联式容器
关联式容器也是用来存储数据的,与序列式容器不同的是,其里面存储的是<key, value>结构的
键值对,在数据检索时比序列式容器效率更高。
二、键值对
用来表示具有一一对应关系一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。

比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
在SGI-STL中关于键值对的定义如下:
pair是一个结构体模板,,有两个公有成员函数first->key,second->value。T1为key的类型,T2为value的类型。
template <class T1, class T2>
struct pair
{typedef T1 first_type;typedef T2 second_type;T1 first;T2 second;pair() : first(T1()), second(T2()){}pair(const T1& a, const T2& b) : first(a), second(b){}
};如何定义一个pair?
pair的构造函数:

void test_pair()
{// 空参// 有名对象pair<int, int> p;pair<double, double> pd;pair<char, char> pc;// 带参pair<int, int> p2(1, 2);// 赋值拷贝构造pair<int, int> p3(p2);cout << p.first << " " << p.second << endl;cout << pd.first << " " << pd.second << endl;cout << pc.first << " " << pc.second << endl;cout << p2.first << " " << p2.second << endl;cout << p3.first << " " << p3.second << endl;// 无名对象cout << "无名对象: " << pair<int, int>(1, 3).first << " " << pair<int, int>(1, 3).second << endl;// C++11 -- 用{ }来构造对象pair<int, int> p4 = { 2, 4 };cout << p4.first << " " << p4.second << endl;// C++98 -- 用make_pair()函数来构造函数pair<int, int> p5 = make_pair(8, 8);cout << p5.first << " " << p5.second << endl;}输出结果:


补充:make_pair函数

三、树形结构的关联式容器
根据应用场景的不同,C++STL总共实现了两种不同结构的关联式容器:树型结构和哈希结构。
| 关联式容器 | 容器结构 | 底层实现 |
|---|---|---|
| set、map、multiset、multimap | 树型结构 | 平衡搜索树(红黑树) |
| unordered_set、unordered_map、unordered_multiset、unordered_multimap | 哈希结构 | 哈希表 |
其中,树型结构容器中的元素是一个有序的序列,而哈希结构容器中的元素是一个无序的序列。
四、set
set的介绍

- set是按照一定次序存储元素的容器,使用set的迭代器遍历set中的元素,可以得到有序序列。
- set当中存储元素的value都是唯一的,不可以重复,因此可以使用set进行去重。
- 与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放value,但在底层实际存放的是由<value, value>构成的键值对,因此在set容器中插入元素时,只需要插入value即可,不需要构造键值对。
- set中的元素不能被修改,因为set在底层是用二叉搜索树来实现的,若是对二叉搜索树当中某个结点的值进行了修改,那么这棵树将不再是二叉搜索树。
- 在内部,set中的元素总是按照其内部比较对象所指示的特定严格弱排序准则进行排序。当不传入内部比较对象时,set中的元素默认按照小于来比较。
- set容器通过key访问单个元素的速度通常比unordered_set容器慢,但set容器允许根据顺序对元素进行直接迭代。
- set在底层是用平衡搜索树(红黑树)实现的,所以在set当中查找某个元素的时间复杂度为logN。
set的定义

1、构造某种类型的空set容器
set<int> s1; //构造int类型的空容器2、使用一段迭代器区间构造set容器
string str("abcdef");
set<char> s3(str.begin(), str.end()); //构造string对象某段区间的复制品3、拷贝构造一个set对象
set<int> s2(s1); //拷贝构造int类型s1容器的复制品4、构造通过不同比较方式的set容器
set < int, greater<int>> s4; //构造int类型的空容器,比较方式指定为大于set的使用
set当中常用的成员函数如下:
| 成员函数 | 功能 |
|---|---|
| insert | 插入指定元素 |
| erase | 删除指定元素 |
| find | 查找指定元素 |
| size | 获取容器中元素的个数 |
| empty | 判断容器是否为空 |
| clear | 清空容器 |
| swap | 交换两个容器中的数据 |
| count | 获取容器中指定元素值的元素个数 |
set当中迭代器相关函数如下:
| 成员函数 | 功能 |
|---|---|
| begin | 获取容器中第一个元素的正向迭代器 |
| end | 获取容器中最后一个元素下一个位置的正向迭代器 |
| rbegin | 获取容器中最后一个元素的反向迭代器 |
| rend | 获取容器中第一个元素前一个位置的反向迭代器 |
使用示例:
int main()
{// set容器底层是平衡搜索树(红黑树)// 所以在插入数据时就会按照中序排序来对数据进行排序// 同时set容器还对数据进行去重sset<int> s;s.insert(1);s.insert(1);s.insert(1);s.insert(3);s.insert(2);s.insert(6);s.insert(4);s.insert(8);s.insert(5);s.insert(5);s.insert(9);set<int>::iterator it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;// 直接删除数据就不用考虑数据是否在数据队列中// 在就直接删除// 不在就不用管s.erase(1);set<int>::iterator it1 = s.begin();while (it1 != s.end()){cout << *it1 << " ";++it1;}cout << endl;// 通过find()来找到对应数据并返回指向当前数据的迭代器// 如果要找的数据并不在当前数据队列中,则报错set<int>::iterator Fit = s.find(8);cout << *Fit << endl;s.erase(Fit);set<int>::iterator it2 = s.begin();while (it2 != s.end()){cout << *it2 << " ";++it2;}set<int> s;s.insert(1);s.insert(1);s.insert(1);s.insert(3);s.insert(2);s.insert(6);s.insert(4);s.insert(8);s.insert(5);s.insert(5);s.insert(9);for (auto e : s){cout << e << " ";}cout << endl;// 找到大于或等于value的值并返回指向该值的迭代器// 找不到就返回指向大于value值的迭代器(大于value的第一个值)auto Sit1 = s.lower_bound(1);cout << *Sit1 << endl;auto Sit2 = s.lower_bound(7);cout << *Sit2 << endl;// 找到大于value的值并返回指向该值的迭代器// 找不到就返回指向大于value值的迭代器(大于value的第一个值)auto Fit1 = s.upper_bound(6);cout << *Fit1 << endl;auto Fit2 = s.upper_bound(7);cout << *Fit2 << endl;cout << s.count(1) << endl;cout << s.count(7) << endl;return 0;
}
五、multiset
multiset的介绍

- multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
- 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value,value>组成的键值对,因此value本身就是key,key就是value,类型为T)。multiset元素的值不能在容器中进行修改(因为元素总是const的),但可以从容器中插入或删除。
- 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则进行排序。
- multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时会得到一个有序序列。
- multiset底层结构为二叉搜索树(红黑树)。
multiset的使用
multiset容器和set容器所提供的成员函数的接口都是基本一致的,这里就不再列举了,multiset容器和set容器的唯一区别就是,multiset允许键值冗余,即multiset容器当中存储的元素是可以重复的。
由于multiset容器允许键值冗余,因此两个容器中成员函数find和count的意义也有所不同:
| 成员函数find | 功能 |
|---|---|
| set对象 | 返回值为val的元素的迭代器 |
| multiset对象 | 返回底层搜索树中序的第一个值为val的元素的迭代器 |
| 成员函数count | 功能 |
|---|---|
| set对象 | 值为val的元素存在则返回1,不存在则返回0(find成员函数可代替) |
| multiset对象 | 返回值为val的元素个数(find成员函数不可代替) |
int main()
{multiset<int> s;s.insert(1);s.insert(1);s.insert(1);s.insert(3);s.insert(2);s.insert(6);s.insert(4);s.insert(8);s.insert(5);s.insert(5);s.insert(9);for (auto e : s){cout << e << " ";}cout << endl;cout << s.count(1) << endl;cout << s.count(5) << endl;return 0;
}六、map
map的介绍
- map是关联式容器,它按照特定的次序(按照key来比较)存储键值key和值value组成的元素,使用map的迭代器遍历map中的元素,可以得到有序序列。
- 在map中,键值key通常用于排序和唯一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,并取别名为pair。
- map容器中元素的键值key不能被修改,但是元素的值value可以被修改,因为map底层的二叉搜索树是根据每个元素的键值key进行构建的,而不是值value。
- 在内部,map中的元素总是按照键值key进行比较排序的。当不传入内部比较对象时,map中元素的键值key默认按照小于来比较。
- map容器通过键值key访问单个元素的速度通常比unordered_map容器慢,但map容器允许根据顺序对元素进行直接迭代。
- map容器支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
- map在底层是用平衡搜索树(红黑树)实现的,所以在map当中查找某个元素的时间复杂度为logN。
map的定义

1、构造某种类型的空set容器
map<int, double> m1; //构造一个key为int类型,value为double类型的空容器2、使用一段迭代器区间构造set容器
map<int, double> m3(m2.begin(), m2.end()); //使用迭代器拷贝构造m2容器某段区间的复制品3、拷贝构造一个set对象
map<int, double> m2(m1); //拷贝构造key为int类型,value为double类型的m1容器的复制品4、构造通过不同比较方式的set容器
map<int, double, greater<int>> m4; //构造一个key为int类型,value为double类型的空容器,key比较方式指定为大于map的使用
map的插入

insert函数的参数
insert函数的参数显示是value_type类型的,实际上value_type就是pair类型的别名。
因此,我们向map容器插入元素时,需要用key和value构造一个pair对象,然后再将pair对象作为参数传入insert函数。
方式一:构造有名对象插入
// 有名对象
map<int, int> m;
pair<int, int>kv = { 1, 1 };
m.insert(kv);方式二:构造匿名对象插入
map<int, int> m;// 无名对象
m.insert(pair<int, int>(2, 4));方式三:调用make_pair函数模板插入
map<int, int> m;// c++98
m.insert(make_pair(4, 6));方式四:用{ }构造对象,其实际做法是多参数的隐式类型转换(基础是有构造函数)。这种用{}构造对象的方法是C++11才支持的。
map<int, int> m;// c++11 -- 用{ }构造对象,其实际做法是多参数的隐式类型转换(基础是有构造函数)
m.insert({ 3, 2 });insert函数的返回值
- 如果插入成功,返回的pair的first是一个迭代器,指向的是新插入的元素。返回的pair的second是一个bool值,成功就返回true;
- 如果插入失败,说明map中已经存在key值相同的pair,那返回的迭代器指向的就是key值相同的pair。返回的pair的second返回的是false。
map的寻找

map的查找函数是根据所给key值在map当中进行查找,若找到了,则返回对应元素的迭代器,若未找到,则返回容器中最后一个元素下一个位置的正向迭代器。
#include <iostream>
#include <string>
#include <map>
using namespace std;int main()
{map<int, string> m;m.insert(make_pair(2, "two"));m.insert(make_pair(1, "one"));m.insert(make_pair(3, "three"));//获取key值为2的元素的迭代器map<int, string>::iterator pos = m.find(2);if (pos != m.end()){cout << pos->second << endl; //two}return 0;
}map的删除

我们既可以根据key值删除指定元素,也可以根据迭代器删除指定元素,若是根据key值进行删除,则返回实际删除的元素个数。
#include <iostream>
#include <string>
#include <map>
using namespace std;int main()
{map<int, string> m;m.insert(make_pair(2, "two"));m.insert(make_pair(1, "one"));m.insert(make_pair(3, "three"));//方式一:根据key值进行删除m.erase(3);//方式二:根据迭代器进行删除map<int, string>::iterator pos = m.find(2);if (pos != m.end()){m.erase(pos);}return 0;
}map的[ ]运算符重载
由于set中的值是单一的,不需要通过key来访问到value,所以set不实现重载[]。

]运算符重载函数的参数就是一个key值,而这个函数的返回值如下:
(*((this->insert(make_pair(k, mapped_type()))).first)).second就这样看着不太好理解,我们整理一下,实际上[ ]运算符重载实现的逻辑实际上就是以下三个步骤:
- 调用insert函数插入键值对。
- 拿出从insert函数获取到的迭代器。
- 返回该迭代器位置元素的值value。
对应分解代码如下:
mapped_type& operator[] (const key_type& k)
{//1、调用insert函数插入键值对pair<iterator, bool> ret = insert(make_pair(k, mapped_type()));//2、拿出从insert函数获取到的迭代器iterator it = ret.first;//3、返回该迭代器位置元素的值valuereturn it->second;
}总结一下:
- 如果k不在map中,则先插入键值对<k, V()>,然后返回该键值对中V对象的引用。
- 如果k已经在map中,则返回键值为k的元素对应的V对象的引用。
map的迭代器遍历
map当中迭代器相关函数如下:
| 成员函数 | 功能 |
|---|---|
| begin | 获取容器中第一个元素的正向迭代器 |
| end | 获取容器中最后一个元素下一个位置的正向迭代器 |
| rbegin | 获取容器中最后一个元素的反向迭代器 |
| rend | 获取容器中第一个元素前一个位置的反向迭代器 |
遍历方式一: 用正向迭代器进行遍历。
#include <iostream>
#include <string>
#include <map>
using namespace std;int main()
{map<int, string> m;m.insert(make_pair(2, "two"));m.insert(make_pair(1, "one"));m.insert(make_pair(3, "three"));//用正向迭代器进行遍历map<int, string>::iterator it = m.begin();while (it != m.end()){cout << "<" << it->first << "," << it->second << ">" << " ";it++;}cout << endl; //<1,one> <2,two> <3,three>return 0;
}遍历方式二: 用反向迭代器进行遍历
#include <iostream>
#include <string>
#include <map>
using namespace std;int main()
{map<int, string> m;m.insert(make_pair(2, "two"));m.insert(make_pair(1, "one"));m.insert(make_pair(3, "three"));//用反向迭代器进行遍历map<int, string>::reverse_iterator rit = m.rbegin();while (rit != m.rend()){cout << "<" << rit->first << "," << rit->second << ">" << " ";rit++;}cout << endl; //<3,three> <2,two> <1,one>return 0;
}遍历方式三: 用范围for进行遍历。
#include <iostream>
#include <string>
#include <map>
using namespace std;int main()
{map<int, string> m;m.insert(make_pair(2, "two"));m.insert(make_pair(1, "one"));m.insert(make_pair(3, "three"));//用范围for进行遍历for (auto e : m){cout << "<" << e.first << "," << e.second << ">" << " ";}cout << endl; //<1,one> <2,two> <3,three>return 0;
}注意: 编译器在编译时会自动将范围for替换为迭代器的形式,因此支持了迭代器实际上就支持了范围for。
map的其他成员函数
除了上述成员函数外,set当中还有如下几个常用的成员函数:
| 成员函数 | 功能 |
|---|---|
| size | 获取容器中元素的个数 |
| empty | 判断容器是否为空 |
| clear | 清空容器 |
| swap | 交换两个容器中的数据 |
| count | 获取容器中指定key值的元素个数 |
七、mltimap
multimap容器与map容器的底层实现一样,也都是平衡搜索树(红黑树),其次,multimap容器和map容器所提供的成员函数的接口都是基本一致的,这里也就不再列举了,multimap容器和map容器的区别与multiset容器和set容器的区别一样,multimap允许键值冗余,即multimap容器当中存储的元素是可以重复的。
#include <iostream>
#include <string>
#include <map>
using namespace std;int main()
{multimap<int, string> mm;//插入元素(允许重复)mm.insert(make_pair(2, "two"));mm.insert(make_pair(2, "double"));mm.insert(make_pair(1, "one"));mm.insert(make_pair(3, "three"));for (auto e : mm){cout << "<" << e.first << "," << e.second << ">" << " ";}cout << endl; //<1,one> <2,two> <2,double> <3,three>return 0;
}由于multimap容器允许键值冗余,因此两个容器中成员函数find和count的意义也有所不同:
| 成员函数find | 功能 |
|---|---|
| map对象 | 返回值为键值为key的元素的迭代器 |
| multimap对象 | 返回底层搜索树中序的第一个键值为key的元素的迭代器 |
| 成员函数count | 功能 |
|---|---|
| map对象 | 键值为key的元素存在则返回1,不存在则返回0(find成员函数可代替) |
| multimap对象 | 返回键值为key的元素个数(find成员函数不可代替) |
其次,由于multimap容器允许键值冗余,调用[ ]运算符重载函数时,应该返回键值为key的哪一个元素的value的引用存在歧义,因此在multimap容器当中没有实现[ ]运算符重载函数。
