map和set的使用
一、序列式容器和关联式容器
前面我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为是逻辑结构为线性序列的数据结构,两个位置存储的值一般没有紧密的关联关系,比如交换一下,它依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。单纯的存储数据。
关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构,两个位置存储的值有紧密的关联关系,交换一下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。不仅仅是存储数据,还要进行搜索。
本章节讲解的map和set底层是红黑树,红黑树是一颗平衡二叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。
二、set系列的使用
1、set和multiset参考文档
2、set类的介绍
set的声明如下,T就是set底层关键字的类型。
set默认要求T支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数。
set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。
一般情况下,我们都不需要传后两个模版参数。
set底层是用红黑树实现,增删查效率是O(logN),迭代器遍历是走的搜索树的中序,所以是有序的。
前面部分我们已经学习了vector/list等容器的使用,STL容器接口设计,高度相似,所以这里我们就不再一个接口一个接口的介绍,而是直接带着大家看文档,挑比较重要的接口进行介绍。
3、set的构造和迭代器
set的构造我们关注以下几个接口即可。
set支持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围for,set的iterator和const_iterator都不支持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。
4、set的增删查
set的增删查关注以下几个接口即可:
key_type、value_type因为底层typedef的原因,都表示第一个模板参数T。
5、insert和迭代器遍历使用样例
set严格来说是"去重+排序",所以使用set来进行去重的操作是非常方便的。
#include<iostream>
#include<set>
using namespace std;
int main()
{
// 去重+升序排序
set<int> s;
// 去重+降序排序(给一个大于的仿函数)
//set<int, greater<int>> s;
s.insert(5);
s.insert(2);
s.insert(7);
s.insert(5);
// auto it = s.begin();
set<int>::iterator it = s.begin();
while (it != s.end())
{
// error,set不支持修改
//*it = 1;
cout << *it << " ";
++it;
}
cout << endl;
// 支持迭代器就支持范围for
//for (auto e : s)
//{
// cout << e << " ";
//}
// 插⼊⼀段initializer_list列表值,已经存在的值插⼊失败
s.insert({ 2, 3, 6, 8 });
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
set<string> strset = { "sort", "insert", "add" };
// 遍历string⽐较ascll码⼤⼩顺序遍历的
for (auto& e : strset)
{
cout << e << " ";
}
cout << endl;
return 0;
}
6、find和erase使用样例
#include<iostream>
#include<set>
using namespace std;
int main()
{
set<int> s = { 4,2,7,2,8,5,9 };
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 删除最小值。删除的是搜索二叉树中的最左结点,即值最小结点
s.erase(s.begin());
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 直接删除x
int x;
cin >> x;
int num = s.erase(x);// 返回值是整型是为了兼容multiset中erase的用法
if (num == 0)
{
cout << x << "不存在!" << endl;
}
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 或直接查找再利用迭代器删除x
cin >> x;
auto pos = s.find(x);
if (pos != s.end())
{
s.erase(pos);
}
else
{
cout << x << "不存在!" << endl;
}
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 两个查找的区别:
// 算法库的查找是暴力查找 O(N),算法库里的find是为了vector、list这样的容器准备的
auto pos1 = find(s.begin(), s.end(), x);
// set自身实现的查找 O(logN)
auto pos2 = s.find(x);
// 利用count间接实现快速查找
cin >> x;
/*auto pos = s.find(x);
if (pos != s.end())*/
if (s.count(x))
{
cout << x << "存在" << endl;
}
else
{
cout << x << "不存在" << endl;
}
return 0;
}
把一段闭区间给lower-bound(下限)、upper-bound(上限),它们可以利用迭代器找一段set容器里的左闭右开的区间。
#include<iostream>
#include<set>
using namespace std;
int main()
{
std::set<int> myset;
for (int i = 1; i < 10; i++)
myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90
for (auto e : myset)
{
cout << e << " ";
}
cout << endl;
// 实现查找到的[itlow, itup)包含[30, 60]区间
// 返回 >= 30
auto itlow = myset.lower_bound(30);
// 返回 > 60
auto itup = myset.upper_bound(60);
// 删除这段区间的值
myset.erase(itlow, itup);
for (auto e : myset)
{
cout << e << " ";
}
cout << endl;
return 0;
}
// 实现查找到的[itlow, itup)包含[35, 65]区间
// 返回 >= 35
auto itlow = myset.lower_bound(35);
// 返回 > 65
auto itup = myset.upper_bound(65);
// 删除这段区间的值
myset.erase(itlow, itup);
for (auto e : myset)
{
cout << e << " ";
}
cout << endl;
// 实现查找到的[itlow, itup)包含[35, 90]区间
// 返回 >= 35
auto itlow = myset.lower_bound(35);
// 返回 > 90,返回的是end()
auto itup = myset.upper_bound(90);
// 删除这段区间的值
myset.erase(itlow, itup);
for (auto e : myset)
{
cout << e << " ";
}
cout << endl;
7、multiset和set的差异
multiset和set的使用基本完全类似,主要区别点在于multiset支持值冗余,那么insert/find/count/erase都围绕着支持值冗余有所差异,具体参看下面的样例代码理解。
#include<iostream>
#include<set>
using namespace std;
int main()
{
// 相⽐set不同的是,multiset是排序,但是不去重
multiset<int> s = { 4,2,7,2,4,8,4,5,4,4,9 };
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 相比set不同的是,x可能会存在多个,find查找中序的第一个x
int x;
cin >> x;
auto pos = s.find(x);
while (pos != s.end() && *pos == x)
{
cout << *pos << " ";
++pos;
}
cout << endl;
// 相⽐set不同的是,count会返回x的实际个数
cout << s.count(x) << endl;
// 删除中序的第一个x
/*pos = s.find(x);
if (pos != s.end())
s.erase(pos);*/
// 相⽐set不同的是,erase给值时会删除所有的x
s.erase(x);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
cout << s.erase(5) << endl;
cout << s.erase(6) << endl;
return 0;
}
大量的数据排序用multiset还是不及放到vector里面走sort,虽然时间复杂度是O(logN),但是树形结构、链式结构的空间缓存命中率是不及vector的。
8、算法题
三、map系列的使用
1、map和multimap参考文档
2、map类的介绍
map的声明如下,Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key支持小于比较,如果不支持或者需要的话可以自行实现仿函数传给第二个模版参数,map底层存储数据的内存是从空间配置器申请的。一般情况下,我们都不需要传后两个模版参数。map底层是用红黑树实现,增删查改效率是 O(logN) ,迭代器遍历是走的中序,所以是按key有序顺序遍历的。
3、pair类型介绍
对比之前学习的搜索二叉树,之前两个值是分开存储的,但是map这里是一起存储的。分开存储的话,迭代器没办法解引用,*it
调用operator*
也不能返回两个值,只能一次返回一个值,两个值在一个结点中以一个结构体存储,这个结构体就是pair。pair是类模板的结构体。
map底层的红黑树节点中的数据,使用pair<Key, T>
存储键值对数据。
4、map的构造
map的构造我们关注以下几个接口即可。
map的支持正向和反向迭代器遍历,遍历默认按key的升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围for,map支持修改value数据,不支持修改key数据,因为修改关键字数据,会破坏了底层搜索树的结构。
5、map的增删查
map的增删查关注以下几个接口即可:
map增接口,插入的pair键值对数据,跟set所有不同,但是查和删的接口只用关键字key跟set是完全类似的,不过find返回iterator,不仅仅可以确认key在不在,还找到key映射的value,同时通过迭代器还可以修改value。
6、map的数据修改
前面提到map支持修改 mapped_type 数据,不支持修改 key_type 数据,因为修改关键字数据,会破坏了底层搜索树的结构。
map第一个支持修改的方式是通过迭代器,迭代器遍历是通过find返回key所在的iterator修改,map还有一个非常重要的修改接口operator[],但是operator[]不仅仅支持修改,还支持插入数据和查找数据,所以他是一个多功能复合接口。
需要注意从内部实现角度,map这里把我们传统说的value值,给的是T类型,typedef为mapped_type。而value_type是红黑树结点中存储的pair键值对值。日常使用我们还是习惯将这里的T映射值叫做value。
7、构造遍历及增删查使用样例
有了隐式类型转换以后就不会用make_pair了,写有名/匿名对象太麻烦就会用make_pair,make_pair是一个函数模板,本质就是一个推导,靠传递过来的参数推导类型而不用自己声明模板参数,最后返回pair的匿名对象。
#include <iostream>
#include <map>
using namespace std;
int main()
{
// initializer_list构造及迭代遍历
map<string, string> dict = { { "left", "左边" } , { "right", "右边" } , { "insert", "插入" } , { "string", "字符串" } };
// map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
while (it != dict.end())
{
/* 按照之前的写法会编译不通过,因为pair不支持流插入、流提取,
pair里面的两个值是公有的,可以直接取first和second */
// cout << (*it).first << ":" << (*it).second << endl;
// map的迭代器基本都使用operator->,这里省略了一个->
// 第一个->是迭代器运算符重载,返回pair*,第二个箭头是结构指针解引用取pair数据
// cout << it.operator->()->first << ":" << it.operator->()-> second << endl;
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
/* insert插入pair对象的4种方式,对比之下,最后一种最方便 */
// 1、有名对象——最麻烦
pair<string, string> kv1( "sort", "排序" );
dict.insert(kv1);
// 2、匿名对象——较麻烦
dict.insert(pair<string, string>( "first", "第一个" ));
// 3、make_pair——写有名/匿名对象太麻烦就会用,有了隐式类型转换以后就不会用make_pair了
dict.insert(make_pair("second", "第二个"));
// 4、多(单)参数的隐式类型转换
dict.insert({ "auto", "自动的" });
// "left"已经存在,插入失败
dict.insert({ "left", "左边,剩余" });
// 支持迭代器遍历就可以支持范围for遍历
for (const auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
string str;
while (cin >> str)
{
auto ret = dict.find(str);
if (ret != dict.end())
{
cout << "->" << ret->second << endl;
}
else
{
cout << "无此单词,请重新输入!" << endl;
}
}
// erase等接口跟set完全类似,这里就不演示讲解了
return 0;
}
8、map的迭代器和[]功能样例
统计水果出现的次数(真实的统计次数是不会用二叉搜索树的)。
set的迭代器不能修改值,map的迭代器可以修改Value。
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
// 利用find和iterator修改功能,统计⽔果出现的次数
// 只能用find,不能用count,因为下面涉及修改Value。
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (const auto& str : arr)
{
// 先查找水果在不在map中
// 1、不在,则水果是第一次出现,插入{ 水果,1 }
// 2、在,则查找到的结点中水果对应的次数++
auto ret = countMap.find(str);
if (ret == countMap.end())
{
countMap.insert({ str, 1 });
}
else
{
ret->second++;
}
}
for (const auto& e : countMap)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
return 0;
}
// 上面的一段可以用下面的代码替换
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
// 利用[]插⼊+修改功能,巧妙实现统计水果出现的次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜","苹果", "香蕉", "苹果", "香蕉" };
map<string, int> countMap;
for (const auto& str : arr)
{
// []先查找⽔果在不在map中
// 1、不在,说明⽔果第⼀次出现,则插⼊{⽔果, 0},同时返回次数的引⽤,++⼀下就变成1次了
// 2、在,则返回⽔果对应的次数++
countMap[str]++;
}
for (const auto& e : countMap)
{
cout << e.first << ":" << e.second << endl;
}
cout << endl;
return 0;
}
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
map<string, string> dict;
dict.insert(make_pair("sort", "排序"));
// key不存在->插⼊ {"insert", string()}
dict["insert"];
// 插入+修改
dict["left"] = "左边";
// 修改
dict["left"] = "左边、剩余";
// key存在->查找
cout << dict["left"] << endl;
// 在这里insert一个相同的key值,value是否会被修改?——不会,还是要利用迭代器或者[]修改
dict.insert({ "sort", "排序xxxx" });
return 0;
}
9、multimap和map的差异
multimap和map的使用基本完全类似,主要区别点在于multimap支持关键值key冗余,那么insert/find/count/erase都围绕着支持关键值key冗余有所差异,这里跟set和multiset完全一样,比如find时,有多个key,返回中序第一个。其次就是multimap不支持[],因为支持key冗余,[]就只能支持插入了,不能支持修改,多个相同的key,不确定返回的是哪一个key的value值。
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
multimap<string, string> dict;
dict.insert({ "sort", "排序" });
dict.insert({ "sort", "排序" });
dict.insert({ "sort", "排序xxx" });
dict.insert({ "left", "左边" });
//dict.erase("sort");
// 用来找一段相等的区间
auto itpair = dict.equal_range("sort");// 返回迭代器区间,左闭右开
auto it = itpair.first;
while (it != itpair.second)
{
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
return 0;
}