总言
主要介绍set和map的基本框架和使用
文章目录
- 总言
- 1、部分接口介绍和使用举例
- 1.1、序列式容器和关联式容器、键值对
- 1.1.2、pair键值对
- 1.2、set基本介绍
- 1.2.1、set::set、遍历
- 1.2.2、set::insert、set::erase、set::find
- 1.2.3、set::count、set::lower_bound、set::upper_bound
- 1.2.4、multiset简单演示
- 1.3、map基本介绍
- 1.3.1、map::insert、遍历
- 1.3.2、K-V模型演示:统计事物出现次数:map:find、map::operator[]
- 1.3.2、map::opeator[ ]基本说明,附operator::at简单介绍
- 1.3.4、multimap简单介绍
- 1.4、例题举例
- 1.4.1、例题一: 两个数组的交集
- 1.4.2、例题二:前K个高频单词
- 1.4.2.1、写法一
- 1.4.2.2、写法二
- 1.4.2.3、写法三
1、部分接口介绍和使用举例
1.1、序列式容器和关联式容器、键值对
1.1.2、pair键值对
概念:用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量key
和value
。key代表键值,value表示与key对应的信息。
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){}
};
1.2、set基本介绍
相关参考网址:set
class template
std::set
template < class T, // set::key_type/value_type
class Compare = less<T>, // set::key_compare/value_compare
class Alloc = allocator<T> // set::allocator_type
> class set;
基本说明: set的底层其实是二叉搜素树,模型运用为K模型。
T
: set中存放元素的类型,实际在底层存储<value, value>
的键值对。
Compare
:set中元素默认按照小于来比较less<Key>
Alloc
:set中元素空间的管理方式,实际使用的是STL提供的空间配置器。
PS:less、allocator
为默认缺省参数,和之前博文提及一致,这是为了方便我们在所给默认配置不满足时,构建自己的需求。
1.2.1、set::set、遍历
1)、set的构造函数,以下为部分举例:
//C++98
explicit set (const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());//empty (1)
template <class InputIterator>
set (InputIterator first, InputIterator last,
const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());//range (2)
set (const set& x);//copy (3)
//C++11
set (initializer_list<value_type> il,
const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());//initializer list (5)
相关演示如下:
void test_set_01()
{
//初始化方式一:
set<int> s1;
//插入方式一:
s1.insert(1);
s1.insert(4);
s1.insert(2);
s1.insert(6);
s1.insert(9);
//初始化方式二:initializer list初始化,C++11新增
set<int> s2 = { 3,6,4,5,1,7,3,3,3,4,5,5,5,6,1,7 };
//插入方式三:迭代区间初始化
int arr[] = { 6,5,1,3,4,7,0 };
set<int,greater<int>>s3(arr, arr + sizeof(arr) / sizeof(int));
//迭代器遍历
set<int>::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
//范围for遍历
for (const auto& e : s2)
{
cout << e << " ";
}
cout << endl;
for (const auto& e : s3)
{
cout << e << " ";
}
cout << endl;
}
需要学习注意的点:
1、set的初始化方式:上述举例了三种写法
2、set中迭代器遍历:上述举例了正向迭代器和范围for
3、为什么set默认得到的结果为有序值,且无重复项?
回答:set底层结构为二叉搜素树,二叉搜索树基本框架我们在之前博文介绍过。其遍历具有排序和去重作用。
4、观察set类,其模板类型class Compare = less<T>
中默认为升序,如果要得到降序结果,该如何做?
回答:①可以使用greater<T>
,修改Compare
值;②可以使用返向迭代器。需要注意二者区别,前者实际作用于set类,后者只是在遍历显示时给出效果。
5、关于set只允许增删查,不允许改,相关演示如下:
1.2.2、set::insert、set::erase、set::find
1)、set::insert
inset中,单个元素的插入我们在上一小节中演示过。其它接口可结合相关文档示例学习。
//C++98
pair<iterator,bool> insert (const value_type& val);//single element (1)
iterator insert (iterator position, const value_type& val);//with hint (2)
template <class InputIterator>
void insert (InputIterator first, InputIterator last);//range (3)
2)、set::erase、set::find
相关链接:set::erase
//C++98
void erase (iterator position);//(1)
size_type erase (const value_type& val);//(2)
void erase (iterator first, iterator last);//(3)
相关链接:set:find
//C++98
iterator find (const value_type& val) const;
演示示例如下:
void test_set_02()
{
set<int> s1 = { 3,6,4,5,1,7,2,8,9,0 };
for (const auto& e : s1)
{
cout << e << " ";
}
cout << endl;
//演示一:给定val值删除
s1.erase(4);
for (const auto& e : s1)
{
cout << e << " ";
}
cout << endl;
//演示二:结合find,给定位置删除
set<int>::iterator ret=s1.find(8);
s1.erase(ret);
for (const auto& e : s1)
{
cout << e << " ";
}
cout << endl;
}
需要学习注意的点:
1、erase、find的基本使用;
2、观察上述两种删除方法,似乎并无区别,但实际上存在如下情况:当我们实参传入一个不存在的值时,在有些编译器下可能存在未定义行为。
set<int> s1 = { 3,6,4,5,1,7,2,8,9,0 };
for (const auto& e : s1)
{
cout << e << " ";
}
cout << endl;
//演示一:给定val值删除
s1.erase(44);
for (const auto& e : s1)
{
cout << e << " ";
}
cout << endl;
针对上述情况,假如使用set::find,就可以根据其返回值,进行约束判断:
set<int> s1 = { 3,6,4,5,1,7,2,8,9,0 };
for (const auto& e : s1)
{
cout << e << " ";
}
cout << endl;
//演示二:结合find,给定位置删除
set<int>::iterator ret=s1.find(8);
if (ret != s1.end())
{
s1.erase(ret);
for (const auto& e : s1)
{
cout << e << " ";
}
cout << endl;
}
1.2.3、set::count、set::lower_bound、set::upper_bound
1)、set::count
相比于set
,count
在multiset
中实际作用更大。
此处举例文档样例:
void test_set_03()
{
std::set<int> myset;
// set some initial values:
for (int i = 1; i < 5; ++i)
myset.insert(i * 3); // set: 3 6 9 12
for (int i = 0; i < 10; ++i)
{
std::cout << i;
if (myset.count(i) != 0)
std::cout << " is an element of myset.\n";
else
std::cout << " is not an element of myset.\n";
}
}
2)、set::lower_bound、set::upper_bound
验证代码:
void test_set_04()
{
std::set<int> myset;
std::set<int>::iterator itlow, itup;
for (int i = 1; i < 10; i++) myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90
std::cout << "myset contains:";
for (std::set<int>::iterator it = myset.begin(); it != myset.end(); ++it)
std::cout << ' ' << *it;
std::cout << '\n';
itlow = myset.lower_bound(35);
itup = myset.upper_bound(70);
printf("ltlow,ltup:[%d,%d)\n", *itlow, *itup);
myset.erase(itlow, itup);
std::cout << "myset contains:";
for (std::set<int>::iterator it = myset.begin(); it != myset.end(); ++it)
std::cout << ' ' << *it;
std::cout << '\n';
}
演示结果如下:
lower_bound中,返回值>=val;
upper_bound中,返回值>val;
正因为此,使用erase迭代区间删除,可以满足左闭右开。
1.2.4、multiset简单演示
相关文档链接:multiset
1)、multiset和set的一些函数区别说明
演示一:键值冗余,重复项验证
验证代码如下:
void test_multiset_01()
{
int a[] = { 3,6,4,5,1,7,3,3,3,4,5,5,5,6,1,7 };
multiset<int> s(a, a + sizeof(a) / sizeof(int));
// 排序
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
演示结果如下:
演示二:演示count、find、erase
验证代码如下:
void test_multiset_01()
{
int a[] = { 3,6,4,5,1,7,3,3,3,4,5,5,5,6,1,7 };
multiset<int> s(a, a + sizeof(a) / sizeof(int));
for (auto e : s) { cout << e << " "; }
cout << endl;
cout << "------------------------------------------------" << endl;
//count:
printf("count(3):%d count(7):%d count(2):%d\n", s.count(3), s.count(7), s.count(2));
cout << "------------------------------------------------" << endl;
//find:如果有多个值,返回中序的第一个
auto pos = s.find(7);
printf("find(7):");
while (pos != s.end())
{
cout << *pos << " ";
++pos;
}
cout << endl;
pos = s.find(5);
printf("find(5):");
while (pos != s.end())
{
cout << *pos << " ";
++pos;
}
cout << endl;
cout << "------------------------------------------------" << endl;
//erase:删除所有给定项
printf("erase(3):");
s.erase(3);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
printf("erase(1):");
pos = s.find(1);
if (pos != s.end())
{
s.erase(pos);
}
for (auto e : s) { cout << e << " "; }
cout << endl;
}
验证count:
//count:
printf("count(3):%d count(7):%d count(2):%d\n", s.count(3), s.count(7), s.count(2));
cout << "------------------------------------------------" << endl;
验证find: 在有多个相同项时,其返回中序的第一个。
//find:如果有多个值,返回中序的第一个
auto pos = s.find(7);
printf("find(7):");
while (pos != s.end())
{
cout << *pos << " ";
++pos;
}
cout << endl;
pos = s.find(5);
printf("find(5):");
while (pos != s.end())
{
cout << *pos << " ";
++pos;
}
cout << endl;
cout << "------------------------------------------------" << endl;
验证erase: 给迭代器位置和给值,两种删除结果不同。
//erase:删除所有给定项
printf("erase(3):");
s.erase(3);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
printf("erase(1):");
pos = s.find(1);
if (pos != s.end())
{
s.erase(pos);
}
for (auto e : s) { cout << e << " "; }
cout << endl;
1.3、map基本介绍
相关参考网址:map
class template
std::map
template < class Key, // map::key_type
class T, // map::mapped_type
class Compare = less<Key>, // map::key_compare
class Alloc = allocator<pair<const Key,T> > // map::allocator_type
> class map;
基本说明:
key
: 键值对中key的类型。
T
: 键值对中value的类型。
Compare
: 比较器的类型,map中的元素是按照key来比较的,缺省情况下按照小于来比较,一般情况下(内置类型元素)该参数不需要传递,如果无法比较时(自定义类型),需要用户自己显式传递比较规则(一般情况下按照函数指针或者仿函数来传递)。
Alloc
:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的空间配置器。
1.3.1、map::insert、遍历
1)、map::insert基本介绍
首先要知道map底层相当于二叉搜索树的KV模型。我们来观察其insert:相关链接
由上图可知,map::insert插入时,我们给定的是键值对,即一个结构体模型。
2)、map::insert 如何插入键值对?
相关演示如下:这里列举两种构造方法,可以先使用pair定义出一个对象,再用其插入;也可以直接使用一个匿名对象。
void test_map_01()
{
map<string, string> dict;
//方法演示一:
pair<string, string> kv1("misery", " n.痛苦");//单参数函数支持隐式类型转换
dict.insert(kv1);
//方法演示二:
dict.insert(pair<string, string>("fondness", " n.爱好"));
dict.insert(pair<string, string>("prosperity", " n.幸运"));
dict.insert(pair<string, string>("dragon", " n.龙"));
typedef pair<string, string> DictKV;//若写起来很长,可重命名
dict.insert(DictKV("dragon", " n.龙"));//验证重复项
dict.insert(DictKV("dragon", " n.凶恶的人"));//验证Key值相同,value不同,会不会插入。
//方法演示三:
dict.insert(make_pair("learner", " n.学习者"));//make_pair使用
}
1、关于make_pair说明:其底层也在调用pair构造,相对于模板实例化,它写起来更方便些。
2、insert插入是以key作为判断标准的。想验结果验证在后序遍历中。
dict.insert(DictKV("dragon", " n.龙"));//验证重复项
dict.insert(DictKV("dragon", " n.凶恶的人"));//验证Key值相同,value不同,会不会插入。
3)、接上述2)中样例,如何遍历map?
如下,按照往常一样使用下述形式遍历,会发现编译报错:
map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
cout << *it << endl;//error
++it;
}
因此需要做一些修改:
遍历一:迭代器
map<string, string>::iterator it1 = dict.begin();
while (it1 != dict.end())
{
cout << (*it1).first << ":" << (*it1).second << endl;
++it1;
}
cout << endl;
map<string, string>::iterator it2 = dict.begin();
while (it2 != dict.end())
{
//我们也可以用下标寻址符
cout << it2->first << ":" << it2->second << endl;
++it2;
}
cout << endl;
cout << (*it).first << ":" << (*it).second << endl;
cout << it->first << ":" << it->second << endl;
*it
可得pair对象,.
可访问结构体成员first、second
;实际上我们也能用->
来访问。it->first
是it->->first
的优化:第一个->是运算符重载,返回pair*,第二个->是访问。
我们也可以使用范围:const auto& kv
,由于kv获取到的类型为pair是一个类,故此处为了减少拷贝构造的消耗,使用了引用+const。
//范围for
for (const auto& kv : dict)
{
cout << kv.first << ": " << kv.second << endl;
}
1.3.2、K-V模型演示:统计事物出现次数:map:find、map::operator[]
如下:给定一组天气,如何获取每个天气出现几次?
string arr[] = { "晴","多云","晴","阴","小雨","多云","多云","阴","晴","小雨","大雨","阴","多云","晴" };
1)、方法演示一:同二叉搜索树中学习的写法
//次数统计:写法一
string arr[] = { "晴","多云","晴","阴","小雨","多云","多云","阴","晴","小雨","大雨","阴","多云","晴" };
map<string, int> countMap;
for (auto& str : arr)//直接借助范围for遍历
{
map<string, int>::iterator it = countMap.find(str);
if (it!=countMap.end())//在map中找到相应值:说明非首次出现
{
it->second++;//也可以用:(*it).second
}
else
{
countMap.insert(pair<string, int>(str, 1));//也可以用:make_pair(str,1)
}
}
//范围for
for (const auto& kv : countMap)
{
cout << kv.first << ": " << kv.second << endl;
}
说明:
1、map::find
,注意观察其参数类型,其余基本逻辑写法保持不变。
2)、方法演示二:使用map::operator[]
vector、string中operator[]
是用于连续物理空间下的下标访问,在map这类物理空间不连续的容器中,operator[]
该接口的作用是什么?相关文档链接
观察其参数和返回类型,可以看到operator[]
是给定key值,返回value值。那么,我们也可以利用其达到上述次数统计的作用:
//次数统计·写法二
string arr[] = { "晴","多云","晴","阴","小雨","多云","多云","阴","晴","小雨","大雨","阴","多云","晴" };
map<string, int> countMap;
for (auto& str : arr)//直接借助范围for遍历
{
countMap[str]++;//使用operator[]
}
//范围for
for (const auto& kv : countMap)
{
cout << kv.first << ": " << kv.second << endl;
}
结果如下:
1.3.2、map::opeator[ ]基本说明,附operator::at简单介绍
1)、对于次数统计,map::operator[ ]做了什么?
接上一小节代码进行讲解分析:
for (auto& str : arr)//直接借助范围for遍历
{
countMap[str]++;//使用operator[]
}
一个问题是:若countMap中,key值已经存在(即,我们的天气已经存入map的情况),使用operator[]能找到它并返回。如果key值不存在countMap中,为什么也能做到次数统计?
根据上述说明,可将operator[]的行为模式分为两类:
情况一:当key值存在时,operator[]
充当查找+返回value值(可修改)
的功效;
情况二:当key值不存在时,operator[]
充当插入+返回value值(可修改)
的功效。此处插入的是一个pair键值对,即pari(key,V())
(使用的是匿名构造),其返回值是当前插入的value。
void test_map_03()
{
map<string, string> dict;
dict.insert(make_pair("dictionary", "n.字典"));
dict["onward"];
dict["onward"]="adj.前进的";
dict["dream"]="n.梦想";
}
2)、operator[]底层实现分析
回到上述文档,我们看到文档中说明:
A call to this function is equivalent to:
(*((this->insert(make_pair(k,mapped_type()))).first)).second
那么相当于operator[]底层是调用insert实现的:
mappde_type& operator[](const key_type& k)
{
return (*((this->insert(make_pair(k, mapped_type()))).first)).second;
}
解析一:先来看看map::insert
中返回值说明:相关文档链接
无论成功或失败(key在map中无或有),都返回pair<iterator, bool>
,而iterator指向的pair<key,value>
是根据当前我们传入key确定的,故而上述代码等价于:
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
ret.first->second;
,①ret.first
,得到pair<iterator, bool>
的第一参数,即iterator
。②->
我们之前有说过其是运算符重载,实际上为(operator->)->second
,故最终我们得到的是pair<K, V>
中的V,即value。
1.3.4、multimap简单介绍
相关文档链接:multimap
注意:
1、相比于map,multimap就没有operator[],因为可以键值冗余。
2、 multimap中的key可以重复。故而不能像map一样能达到次数统计效果。
1.4、例题举例
1.4.1、例题一: 两个数组的交集
题目链接
思路一:
将其中一个数组放入set中,另一个数组作为参考查询,依次比较set中是否有匹配的key,此方法需要注意,如果直接使用vector接受交集元素,会存在元素重复问题,如样例中的nums1 = [1,2,2,1], nums2 = [2,2]
。因此可以借助另一个set完成去重,或者使用算法库中的unique。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
set<int> set1(nums1.begin(),nums1.end());//用nums1构建set1
set<int> set2;//解决后序找到的交集元素为重复项的问题
for(int i=0;i<nums2.size();++i)
{
set<int>::iterator it=set1.find(nums2[i]);
if(it!=set1.end())
{
set2.insert(*it);
}
}
//经过set2去重,能保证返回的vector中相同交集元素无重复项。
vector<int> ret(set2.begin(),set2.end());
return ret;
}
};
思路二:
在有序的情况下,分别遍历两个集合A、B,取数,对元素小的集合向后遍历,若两集合中当前数相等,则为交集。
其它:若求差集,则相等的同时向后遍历,元素小的就是差集。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
vector<int> ret;
set<int> set1(nums1.begin(),nums1.end());
set<int> set2(nums2.begin(),nums2.end());
set<int>::iterator it1,it2;
it1=set1.begin();it2=set2.begin();
while(it1!=set1.end()&&it2!=set2.end())
{
if(*it1<*it2)
{
++it1;
}
else if(*it2<*it1)
{
++it2;
}
else
{
ret.push_back(*it1);
++it1;
++it2;
}
}
return ret;
}
};
1.4.2、例题二:前K个高频单词
题目链接
题目分析:这相当于一个top-K 问题,但相比之下其比较排序有两重条件。第一次序看value值,统计单词频率;第二次序看字母大小,字母小的排在前面。
1.4.2.1、写法一
思路说明:
1、使用map统计次数;
2、使用priority_queue排序;
class Solution {
public:
struct Less
{
bool operator()(const pair<string,int>& n1,const pair<string,int>& n2)
{
//第一层比较:比较单词出现频率,value
if(n1.second<n2.second)
return true;
//第二层比较:当单词出现频率相同时,以字典顺序排序,ASCII小的在前被取出,大堆,即小的要尽量往堆顶靠
if(n1.second==n2.second && n1.first>n2.first)
return true;
return false;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
map<string,int> countMap;
//次数统计
for(const auto& str:words)
{
countMap[str]++;
}
//排序
typedef priority_queue<pair<string,int>,vector<pair<string,int>>,Less> MaxHeap;
MaxHeap hp(countMap.begin(),countMap.end());
//取Top—K个元素返回:此处没有使用Max_K-小堆-降序
vector<string> ret;
while(k--)
{
ret.push_back(hp.top().first);
hp.pop();
}
return ret;
}
};
注意事项:
1、使用了多个容器,注意它们各自的参数类型和接口功能。
2、关于排序:由于此处并非大量数据处理,因此我们可以直接将给定的N个数据都用于建大堆,再从堆中取出前K个元素即可。
3、Less仿函数:priority_queue的仿函数相关实现我们之前讲过。这里根据需求,直接使用默认的Compare,所得结果不能满足需求,因此需要我们自己实现。需要注意第二次序的逻辑。
std::priority_queue
template <class T, class Container = vector<T>, class Compare = less<typename Container::value_type> > class priority_queue;
1.4.2.2、写法二
思路说明:
1、实际上,map中默认以key排序,也就意味着我们创建的map<string,int>,默认得到的string就是字典顺序。
2、此时,假如我们使用一个稳定性好的排序,就可以达到对value值(单词频率)排序,且不改变其字典顺序。
class Solution {
public:
struct Greater
{
bool operator()(const pair<string,int>& n1,const pair<string,int>& n2)
{
if(n1.second>n2.second)
return true;
if(n1.second==n2.second && n1.first<n2.first)
return true;
return false;
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
//统计次数
map<string,int> countMap;
int n=words.size();
for(const auto& str : words)
{
countMap[str]++;
}
//排序:需要使用一个稳定的排序算法
vector<pair<string,int>> countV(countMap.begin(),countMap.end());
sort(countV.begin(),countV.end(),Greater());
//取前K个数返回
vector<string> ret;
for(int i=0; i<k; ++i)
{
ret.push_back(countV[i].first);
}
return ret;
}
};
1.4.2.3、写法三
思路说明:
1、在上述两种情况中,我们创建的是pair<key,value>
键值对,map默认排序以pair.first,也就是key排序,这样得到的map默认按照字典顺序。假如我们在此基础上使用pair<value,key>
,就能得到以value排序的序列。但需要注意,如果此时容器使用map,会将value值相同的单词去重,但我们可以使用multimap解决。
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
//统计:此处string已有序
map<string,int> countMap;
for(auto& str:words)
{
countMap[str]++;
}
//使用multimap进行排序:针对value
multimap<int,string,greater<int>> SortMap;
for(auto& kv:countMap)
{
SortMap.insert(make_pair(kv.second,kv.first));
}
//取前K个值
vector<string> ret;
multimap<int,string,greater<int>>::iterator it=SortMap.begin();
for(int i=0;i<k;++i)
{
ret.push_back(it->second);
++it;
}
return ret;
}
};