map和set的介绍和使用
一、关联式容器
关联式容器和序列式容器是C++ STL中的两种不同类型的容器。
-
关联式容器是基于键值对的容器,其中每个元素都有一个唯一的键值,可以通过键值来访问元素。关联式容器包括set、multiset、map和multimap。
-
序列式容器是基于元素序列的容器,其中元素按照一定的顺序排列,可以通过元素的位置来访问元素。序列式容器包括vector、deque、list和array。
-
关联式容器和序列式容器的主要区别在于它们的存储方式和访问方式。关联式容器使用二叉搜索树等数据结构来存储元素,可以快速地查找元素,但是插入和删除元素的效率较低。序列式容器使用数组或链表等数据结构来存储元素,可以快速地插入和删除元素,但是查找元素的效率较低。
在选择使用关联式容器还是序列式容器时,需要根据具体的需求来进行选择。如果需要按照键值来查找并访问元素,可以选择关联式容器;如果需要按照元素的位置来访问元素,可以选择序列式容器。
- 根据应用场景的不同,STL总共实现了两种不同结构的关联式容器:树型结构与哈希结构。
- 树型结构的关联式容器主要有四种:set、multiset、map、multimap。
- 这四种容器的共同点是:使用平衡搜索树(即红黑树)作为其底层结果,容器中的元素是一个有序的序列。
二、键值对
用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表键值,value表示与key对应的信息。比如:现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该英文单词,在词典中就可以找到与其对应的中文含义。
在C++中的键值对结构是pair,它是一个类模版。下面是SGI-STL中关于键值对的定义:
template <class T1, class T2>
struct pair
{
typedef T1 first_type;
typedef T2 second_type;
T1 first; //代表键值key
T2 second; //表示与key对应的信息value
pair(): first(T1()), second(T2())
{}
pair(const T1& a, const T2& b): first(a), second(b)
{}
};
注意:pair类在头文件<utility>中声明,<utility>又被头文件<map>包含。
三、set的介绍和使用
3.1 set的介绍
-
set是按照一定次序存储元素的容器,底层实际就是平衡二叉搜索树(红黑树)的K模型。
-
set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
-
在内部,set中的元素总是按照其内部比较对象(仿函数)所指示的特定严格弱排序准则进行排序。
注意:
- set中只放键值key,插入元素时,只需要插入key即可,不需要构造键值对。
- set中的元素不可以重复,因此可以使用set进行去重。
- 使用set的迭代器遍历set中的元素,可以得到有序序列。
- set中查找某个元素,时间复杂度为:
log_2 n
- set中的元素不允许修改,因为修改set中的元素会破坏二叉搜索树结构。
set的模板参数列表
- T: set中存放元素的类型,也就是键值key的类型。
- Compare:比较对象(仿函数),set中元素默认按照小于来比较。
- Alloc:set中元素空间的管理方式,默认使用STL提供的空间配置器管理。
3.2 set的使用
3.2.1 Constructor
3.2.2 Iterator
注意:set的迭代器是按照搜索二叉树的中序遍历顺序遍历set中的元素,所以当比较对象默认为Less时:set正向迭代器的遍历结果是升序序列;逆向迭代器的遍历结果是降序序列;
3.2.3 Modifiers
insert && erase
swap
调用set::swap完成交换后,容器中的元素是调用前 x 中的元素。所有的迭代器,引用,指针对交换后的对象仍然有效。
底层原理:set::swap是浅交换,只是将两个set中的root指针做了交换。
3.2.4 Operations
find
返回值:找到了返回元素的迭代器;找不到返回set::end;
count
lower_bound && upper_bound
返回值:返回容器中>=val
的第一个元素的迭代器
返回值:返回容器中>val
的第一个元素的迭代器
equal_range
返回值:用键值对pair返回容器中等于val的元素的迭代器范围,其中first<=val
,second>val
;
3.3 multiset的介绍和使用
multiset:允许存在键值相等的元素(允许键值冗余)
multiset和set拥有相同的成员函数,但用法略有不同:
- multiset::find:如果有多个键值相等的元素,返回中序遍历中的第一个。
- multiset::erase(val):如果有多个键值相等的元素,将所有值为val的元素全部删除。
- multiset::count(val):统计容器中值为val的元素个数。
测试代码:
void Test1(){
multiset<int> ms = {1,4,2,6,3,7,4,2,4,1}; //c++11语法:初始化列表构造
multiset<int>::iterator it = ms.begin();
while(it != ms.end())
{
cout << *it << " ";
++it;
}
cout << endl;
cout << "test_erase:";
ms.erase(1);
it = ms.begin();
//......
//遍历输出ms
cout << endl;
cout << "test_find:";
auto pos = ms.find(4);
//......
//从pos开始遍历输出ms
cout << endl;
cout << "test_count:";
cout << ms.count(4) << endl;
}
运行结果:
四、map的介绍和使用
4.1 map的介绍
-
map是按照特定次序存储<key,value>键值对的容器,底层实际就是平衡二叉搜索树(红黑树)的KV模型。
-
在map中,键值key通常用于排序和惟一标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,value_type是键值对pair类型。
-
在内部,map中的元素总是按照键值key进行比较排序的。
map的模版参数列表
- key: 键值对中key的类型
- T: 键值对中value的类型
- Compare: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)
- Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器
注意:在使用map时,需要包含头文件<map>。
4.2 map的使用
map和set的使用大体相同,不做过多介绍。举两个例子具体来看:在上一章节《二叉搜索树》的KV模型应用部分,有两个应用实例:用二叉搜索树的KV模型实现字典和单词计数器。现在我们用map来实现:
4.2.1 例一:Dictionary
void Dictionary(){
map<string, string> dict;
//map存储的元素是键值对pair,有以下几种pair的构造插入方法:
//1.匿名对象构造法
dict.insert(pair<string, string>("string", "字符串"));
//2.typedef类型重命名,缩短类型名
typedef pair<string, string> DictKV;
//3.通过make_pair函数模版构造键值对(推荐)
dict.insert(DictKV("peach", "桃子"));
dict.insert(make_pair("dictionary", "字典"));
dict.insert(make_pair("temporary", "短暂的"));
dict.insert(make_pair("declaration", "声明"));
//map的迭代器遍历
//map的迭代器底层是指向pair键值对结构的指针
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin(); //map迭代器的类型过长,通过auto自动识别类型
while(it != dict.end())
{
//cout << (*it).first << ":" << (*it).second << endl; //解引用后通过.访问pair的成员
cout << it->first << ":" << it->second << endl;//直接通过->访问pair的成员(推荐)
++it;
}
//范围for:
//for(pair<const string, string> &e : dict) //map存储的元素是键值对pair,注意key是const类型
for(auto &e : dict) //临时变量e应该取引用,避免发生拷贝构造,浪费资源
{
cout << e.first << ":" << e.second << endl;
}
string input;
while(cin >> input)
{
auto ret = dict.find(input); //传入key值,返回对应元素(pair)的迭代器
if(ret != dict.end()) //找不到返回map::end
{
cout << ret->first << ":" << ret->second << endl;
}
else{
cout << "There is no such word!" << endl;
}
}
}
make_pair
-
make_pair是一个函数模版,作用是利用传入的两个参数构造键值对pair并返回(传值返回)。
-
利用了函数模版的隐式实例化:通过传入的参数类型自动推导pair的模版参数。
-
make_pair在头文件<utility>中声明,<utility>又被头文件<map>包含。
4.2.2 例二:WordCounter
void WordCounter(){
map<string, int> counter;
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉"};
//方法一:上一章讲过的方法
//for(string &e : arr)
//{
// auto ret = counter.find(e);
// if(ret != counter.end())
// {
// ++ret->second;
// }
// else{
// counter.insert(make_pair(e,1));
// }
//}
//方法二:operator[](推荐)
for(string &e : arr)
{
//如果e不在counter中,先插入pair(e,int()),再对返回value(次数)++;
//如果e在counter中,返回value(次数)的引用,次数++;
++counter[e];
}
for(auto &e : counter)
{
cout << e.first << ":" << e.second << endl;
}
}
operator[]
-
map支持operator[],但与vector等连续存储容器的位置下标访问不同,map支持关键字下标访问,[]中填入key值:
-
如果map中有这个key,返回对应value的引用。(查找+修改value)
-
如果map中没有这个key,会插入一个pair(key,value()),并返回新创建的value的引用。(插入+修改value)
-
所谓pair(key,value())使用给定的key和value的默认构造创建的pair键值对。
-
map::at是一个和operator[]功能相似的成员函数。当key存在时和[]有相同的行为;但当key不存在时,at不会创建新元素而是直接抛异常。
-
operator[]的底层实现相当于:
mapped_type& operator[] (const key_type& k) { return (*((this->insert(make_pair(k,mapped_type()))).first)).second; } //下面代码的可读性更高,更简洁。 mapped_type& operator[] (const key_type& k) { pair<iterator, bool> ret = insert(make_pair(k,mapped_type())); return ret.first->second; }
insert
- 如果key已经在map中,插入失败,返回pair(key_iterator, false);
- 如果key不在map中,插入成果,返回pair(newkey_iterator, true);
4.3 multimap的介绍和使用
- multimap和map的唯一不同就是:map中的key是唯一的,而multimap中允许存在键值相等的元素(允许键值冗余)
- 没有重载operator[]:由于键值冗余,无法确定唯一的value。
- multimap中的接口可以参考map,功能都是类似的。
五、OJ练习
692. 前K个高频单词
349. 两个数组的交集