map和set
1.关联式容器
序列式容器:
在初阶,我们接触过STL部分容器,如: vector、list、deque、forward_list(C++11)等,这些容器被称为序列式容器。
原因:这些容器底层结构为线性序列的数据结构,里面存储的是元素本身。
关联式容器:
关联式容器也是用来存储数据的。
原因:关联式容器与序列式容器不同的是,其里面存储的是<key,value>结构的键值对,在数据检索时比序列式容器效率更高,因为数据间具有关联性。
2.键值对
概念:用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key和value,key代表剑指,value表示与key对应的信息。
比如:英汉互译的字典,每个字典中的英文单词与其中文含义保持一一对应的关系,就是key和value保持一一对应的关系。
SGI-STL中关于键值对的定义:
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)
{}
};
3.树形结构的关联式容器
根据场景不同,STL总共实现了两种不同结构的管理式容器:树形结构与哈希结构。
树形结构的关联式容器:map、set、multimap、multiset。
该四种容器的共同点:使用平衡搜索树(即红黑树)作为底层结构,容器中的元素是一个有序的序列。
3.1 set
3.1.1 介绍
3.1.2 特点
-
set是按照一定次序存储元素的容器
-
在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。
set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。 -
在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行
排序。 -
set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对
子集进行直接迭代。 -
set在底层是用二叉搜索树(红黑树)实现的。
注意:
-
与map/multimap不同,map/multimap中存储的是真正的键值对<key, value>,set中只放
value,但在底层实际存放的是由<value, value>构成的键值对。 -
set中插入元素时,只需要插入value即可,不需要构造键值对。
-
set中的元素不可以重复(因此可以使用set进行去重)。
-
使用set的迭代器遍历set中的元素,可以得到有序序列
-
set中的元素默认按照小于来比较
-
set中查找某个元素,时间复杂度为:O(logN)
-
set中的元素不允许修改(为什么?),因为修改之后就不满足AVL树或红黑树的条件了。
-
set中的底层使用二叉搜索树(AVL树或红黑树)来实现
3.1.3 用例
#include <iostream>
#include <set>
using namespace std;
void TestSet()
{
/*用数组array中的元素构造set*/
int array[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0, 1, 3, 5, 7, 9, 2, 4,
6, 8, 0 };
int sz = sizeof(array) / sizeof(array[0]);
int i = 0;
printf(" 用数组array中的元素构造set\n");
for (i = 0; i < sz; i++)cout << array[i] << ' '; cout << endl;
set<int> s(array, array + sizeof(array) / sizeof(array[0]));//迭代器(或原生指针)区间构造
printf("set中元素个数\n");
cout << s.size() << endl;
/* 正向打印set中的元素,从打印结果中可以看出:set可去重*/
printf(" 正向打印set中的元素,从打印结果中可以看出:set可去重\n");
for (auto& e : s)
cout << e << " ";
cout << endl;
/* 使用迭代器逆向打印set中的元素*/
printf(" 使用迭代器逆向打印set中的元素\n");
for (auto it = s.rbegin(); it != s.rend(); ++it)
cout << *it << " ";
cout << endl;
/* set中值为3的元素出现了几次*/
printf(" set中值为3的元素出现了几次\n");
cout << s.count(3) << endl;
}
int main()
{
TestSet();
return 0;
}
3.2 map
3.2.1 介绍
3.2.2 特点
- map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元
素。 - 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:
typedef pair<const key, T> value_type; - 在内部,map中的元素总是按照键值key进行比较排序的。
- map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
- map支持下标访问符,即在[]中放入key,就可以找到与key对应的value。
- map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))
3.2.3 用例
3.2.3.1 统计计数
//一、统计次数
//法1.迭代器
map<string, int> countMap;
for (auto& str : arr)
{
map<string, int>::iterator it = countMap.find(str);
if (it != countMap.end())
{
//(*it).second++;
it->second++;//本质是it-> 先找到Node; 再(it->)->second;找到second;但编译器优化成了it->second;
//不能再写(it->)->second了
}
else
{
countMap.insert(make_pair(str, 1));
}
}
//法2.map的[]重载,以前是支持数组(顺序表)等连续的存储空间的结构使用。
// map的[]的意思变,返回key对应的value的引用。countMap[key]=value;
map<string, int> countMap;
for (auto& str : arr)
{
// 1、str不在countMap中,插入pair(str, int()),然后在对返回value的引用,次数++
// 2、str在countMap中,返回value(次数)的引用,次数++;
countMap[str]++;
}
3.2.4 成员函数/方法
3.2.4.1 operator[]重载
特点:
- map中有这个key时,返回value的引用。(查找,可修改value)
- map中没有这个key时,会插入一个pair(key,V()),并返回value的引用。-其中V()是value类型匿名对象,默认为空。(插入,可修改value)
用例:
map<string, string> dict;
dict.insert(make_pair("sort", "排序"));
dict["insert"];//如果不插入对应的value,则默认插入一个value()
dict["insert"] = "插入";
dict["left"] = "左边";
模拟实现:
mapped _type& operator[](const key_type& k)
{
//本质是return (*((this->insert(make_pair(k,mapped_type()))).first)).second;
return (*((insert(make_pair(k,mapped_type()))).first)).second;
}
//解读:
//一、insert解释
//single element (1) pair<iterator,bool> insert (const value_type& val);
// 简化为:pair<K,V> insert(pair<K,V> val);
//1. key已经在map中,插入失败,返回pair(key_iterator,false); 即返回pair(已经有的key的迭代器,false);
//2.key不在map中,插入成功,返回pair(new_key_iterator,true); 即返回pair(新的key的迭代器,true);
//二、return值的解释
//可拆分为
//pair<iterator,bool> ret=insert(make_pair(key,V()));插入成功返回的是新key的迭代器,插入失败返回的是已有key的迭代器。
//return ret.first ->second; ret是pair<iterator,bool>。ret.first找到pair的iterator,iterator也是pair<key,value>类型的指针。再对(ret.first) ->second是找到迭代器的value。
//疑问:
//为什么不用find+insert实现。因为find要遍历一遍查找,insert又要遍历一遍插入。所以能单纯insert就单纯insert,只需要遍历一遍插入。
3.3 multiset
3.3.1 介绍
是map的一种,在头文件map中,要#include
3.3.2 特点
- multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
- 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value, value>组成的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器中进行修改(因为元素总是const的),但可以从容器中插入或删除。
- 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则进行排序。
- multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时会得到一个有序序列。
- multiset底层结构为二叉搜索树(红黑树)。
3.3.3 用例
用例与multimap类似,区别在于pair<key,value>中的value是空值,不对其进行赋值操作。
3.4 multimap
3.4.1 介绍
是map的一种,在头文件map中,要#include
3.4.2 特点
- 支持键值冗余,因为multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
- 不支持operator[]重载,因为可能有多个key,[]无法知道返回哪个key的value。
- 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value, value>组成的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器中进行修改(因为元素总是const的),但可以从容器中插入或删除。
- 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则进行排序。
- multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭代器遍历时会得到一个有序序列。
- multiset底层结构为二叉搜索树(AVL树或红黑树)。
3.4.3用例
multimap<string, string> mdict;
mdict.insert(make_pair("sort", "排序"));
mdict.insert(make_pair("left", "左边"));
mdict.insert(make_pair("left", "左边"));
mdict.insert(make_pair("left", "剩余"));
3.5 面试题/编程题
- 前K个高频词汇
题目链接:692. 前K个高频单词 - 力扣(LeetCode)
题目描述:
法一 优先级队列+仿函数
struct Less//仿函数 stable_sort/sort传的less是排成升序,priority传的less是排成降序(大堆),注意区分。
//即,sort传的less,若前<后为真则不需要交换,使的前<后。priority传的less,若前<后为真则需要交换,使得前>后。
{
bool operator()(const pair<string,int>& kv1,const pair<string,int>&kv2)const
{
if(kv1.second<kv2.second)//若kv1的次数second<kv2的次数second,则交换,把次数少的往后挪,次数多的往前挪
return true;
if(kv1.second==kv2.second&&kv1.first>kv2.first)//若次数相同,则kv1的字母值大于kv2的字母值,则交换
return true; //把字母小的往前挪,字母大的往后挪
return false;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
map<string,int> countMap;
//统计次数
for(auto& str:words)
{
countMap[str]++;//按字符大小顺序排一次序并且计数
}
//topk
//法1.push构造 优先级队列
// priority_queue<pair<string,int>,vector<pair<string,int>,Less>maxHeap;
// for(auto& kv:countMap)
// {
// maxHeap.push(kv);
// }
//法2.迭代器区间构造 优先级队列
typedef priority_queue<pair<string,int>,vector<pair<string,int>>,Less>MaxHeap;
MaxHeap mh(countMap.begin(),countMap.end());
//传Less优先级队列建大堆,即建起来之后是parent<child,
//或者可以理解为:在建的时候只要满足 前<后,则swap。
//结果:将大的往前挪,将小的往后挪。 即排完之后parent(后的)<child(前的)
vector<string> v;
while(k--)
{
v.push_back(mh.top().first);//取topk
mh.pop();
}
return v;
}
法二 默认排序sort+稳定性仿函数 / stable_sort稳定性排序 (相同的值相对顺序不变)
//stable_sort/sort无法对map进行排序,sort要求是联系物理空间的迭代器,所以先得把map的数据放到vector里。
struct Greater
{
bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2)
{
if(kv1.second>kv2.second)//若为真则不需要交换
return true;
if(kv1.second==kv2.second&&kv1.first<kv2.first)//若为真,则不需要交换--
//这个将默认的不稳定的快排变成了稳定的快排。或者也可以
//把这个给注释掉,直接使用库里稳定的stable_sort排序
return true;
return false;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
map<string,int> countMap;
//统计次数
for(auto& str:words)
{
countMap[str]++;//按字符大小顺序排一次序并且计数
}
vector<pair<string,int>> sortV(countMap.begin(),countMap.end());
sort(sortV.begin(),sortV.end(),Greater());//也可以直接使用库里的stable_sort()进行排序,此时仿函数中可以不用考 //虑相同值的先后顺序,因为stable_sort()会默认进行处理。
vector<string> v;
for(size_t i=0;i<k;i++)
{
v.push_back(sortV[i].first);//取topk
}
return v;
}
法三 multimap
vector<string> topKFrequent(vector<string>& words, int k) {
map<string,int> countMap;
//统计次数
for(auto& str:words)
{
countMap[str]++;//按字符大小顺序排一次序并且计数。
}
multimap<int,string,greater<int>> sortMap;//不能使用map,因为map会去重,可能会舍去很多出现次数相同的单词。
//multimap的greater和less是对于key来说的,也就是现在的int。按降序来排,和sort的less和greater含义相同,
//若 前>后 为真,则不交换。使得大的在前,小的在后。
//这里必须加个greater<int>,因为如果不加的话排出来的是默认按less<int>,这样排出来的multimap sortV是升序的。
//要取次数topk大只能用反向迭代器在sortV里倒着去取,但是因为sortV里的字母的顺序是已经在前面计数+字符大小排序时排好的 //了,倒着去取的话会使字母的顺序逆序。是不行的。
for(auto& kv:countMap)
{
sortMap.insert(make_pair(kv.second,kv.first));//再按出现次数插入multimap排一次序,按出现次数排降序。
}
vector<string> v;
multimap<int,string,greater<int>>::iterator it=sortMap.begin();
for(size_t i=0;i<k;i++)
{
v.push_back(it->second);//取topk
it++;
}
return v;
}
- 两个数组的交集
题目链接:https://leetcode.cn/problems/intersection-of-two-arrays/submissions/
题目描述:
法一 不相同,则小的++;相等,则同时++,并插入vector。最后的vector即为交集。
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
set<int> s1(nums1.begin(),nums1.end());//默认构造set里已经排好序,升序。
set<int> s2(nums2.begin(),nums2.end());
auto it1=s1.begin();
auto it2=s2.begin();
vector<int> v;
while(it1!=s1.end()&&it2!=s2.end())
{
//让小的++,因为小的一定不属于交集。
if(*it1<*it2)//不相等的则为差集
{
it1++;
}
else if(*it2<*it1)
{
it2++;
}
else//找到相等才是交集。
{
v.push_back(*it1);
it1++;
it2++;
}
}
return v;
}