我们要开始学习map和set的使用,虽然使用更加复杂,但是STL整体的设计,本身就具有很强的前瞻性和延续性,比如说迭代器等,我们顺着文档来看。这也是除了vector之外最重要的容器,当然还有unordered_map 和 unord_set,其实这几个也是C++里面用的最多的。
Set学习
模版参数
这里的set呢,它只有一个模版参数可就是T,然后还有一个比较器,默认是小于,它给一个比较器呢,就是为了,有些地方默认的operator <重载的比较不符合你的要求,假设你用的是指针,但是指针是按地址比较大小,当这个T是一个指针的时候,但是你不想按地址比较大小,那么原生的行为不符合怎么办,那你是不是就可以传仿函数呀,去控制里面的比较规则呀。在下面就是空间配置器,它是负责申请内存。
insert()
我们看到函数参数中有一个,value_type类型,但是不知道这个是什么类型。我们先写一个最简单来看看。
》set s;set 没有push() ,线性表、链表、队列等都是线性结构,才会有push()接口 。后面非线性结构的都是insert()接口了。我们随便插入一些数据s.insert(4);s.insert(5)…我们插入5个值。然后我们定义一个迭代器,set::iterator it = s.begin();这个set呢,它是一个双向迭代器,它底层用了特殊的算法迭代,是走的另一种非递归,后面也会讲,不用像我们在前面的二叉树OJ那里用栈模拟,但是它这里非递归有前提的,前提就是,它要有三叉链,它的逻辑底层是有parent成员变量的,反正我们先不管,后面我们才讨论底层。
》然后我们while循环遍历,while(it != s.end()),cout<< *it << " ";it++然后我们看到,走出来就是有序的!其实它不仅仅是有序的,它是在有序的情况下还加了一些东西,如果值已经有了,它还会不会插入?不会了!所以严格来说,它不是排序,而是一个排序➕去重!
》它这块支持迭代器,是不是有普通迭代器支持可读可写,还有const迭代器只读。然后它还有反向迭代器,就不演示了。
》它既然支持迭代器,那么就会支持范围for。其实范围for和我们迭代器在底层上是一样的,只是在上层看来有些区别。
》我们发现,const迭代器和普通迭代器都是不支持修改的 ,虽然一个叫iteratro,一个叫const iterator,但是底层都是用的一个const_iterator来实现的。
》另外重载的两个insert()函数用的很少。平时也很少用在某一个位置插入,这个接口价值很小。
》insert()不会有迭代器失效的问题,不像顺序表,挪动数据会影响相对关系,它的erase()是会有迭代器失效的问题。
void test_set1()
{
set<int> s;
s.insert(4);
s.insert(5);
s.insert(2);
s.insert(1);
s.insert(1);
s.insert(3);
s.insert(2);
s.insert(1);
// 排序 + 去重
set<int>::iterator it = s.begin();
while (it != s.end())
{
//*it = 10;
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
empty()/size()/max_size()
erase()/find()—两个是要配合的
erase()函数主要有两种,一种是在某个位置删除,另外一种是删除某一个具体的值。第三种删除特定的区间,这个用的很少。
》这块要玩删除的话,就得配合find()函数。我们前面插入了很多值,我们随便set::iterator it = find(2)一个值,那么我们**怎么判断是找到还是没找到呢?**如果元素被找到了,是会返回value值对应的迭代器,查找失败就会返回end()迭代器。是不是就是if(pos != s.end())就是找到了呀。
》我们来对比一下,set自带的find()和算法里面的find()有什么区别呢? set::iterator it = find(20) 和 pos = find(s.begin(),s,end(),20),算法里面的find,能不能行呢?是行的。它们的区别很大,数据量小还好,数据量大就会差别很大。set的find()的效率是logN,而算法的查找效率是O(N),因为算法的是暴力查找。这个算法的find()查找不是给set用的,而是给像vector它们用的。
void test_set2()
{
set<int> s;
s.insert(4);
s.insert(5);
s.insert(2);
s.insert(1);
s.insert(1);
s.insert(3);
s.insert(2);
s.insert(1);
set<int>::iterator pos = s.find(20); // O(logN)
if (pos != s.end())
{
cout << "set.find找到了" << endl;
}
pos = find(s.begin(), s.end(), 2); // O(N)
if (pos != s.end())
{
cout << "find找到了" << endl;
}
}
》如果我们要erase了呢?,我想将我从键盘中输入的值erase()掉,通过find()查找我们输入的值再去erase(迭代器)。当erase()成功之后,对应的位置就不能访问了,已经是失效了。我们可以通过查找进行持续的删除。我们还可以直接传参数去删除,即erase(3);也就是你可以理解成,这个erase(3),相当于调用了find()➕erase(迭代器)。
》前面第一种是先是找到了再去删除,它们会有什么区别呢?第二种用法,如果没有找到直接去删除是不会报错的;而你第一种调用了find()没有找到,你还是去删除,是会报错的。
》size_type erase(value)是会返回一个size_type类型的数据,他为什么不用bool类型呢,是因为它和另外讲的东西契合起来。我们能够看到,删除成功是返回1,删除失败是会返回0,为什么是返回这个,而不是返回bool呢?因为待会儿我们还要学另外一个版本,另外一个版本是允许冗余的。
void test_set3()
{
set<int> s;
s.insert(4);
s.insert(5);
s.insert(2);
s.insert(1);
s.insert(1);
s.insert(3);
s.insert(2);
s.insert(1);
cout << s.erase(3) << endl;
cout << s.erase(30) << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
set<int>::iterator pos = s.find(3);
if (pos != s.end())
s.erase(pos);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
/*int x;
while (cin >> x)
{
set<int>::iterator pos = s.find(x);
if (pos != s.end())
{
s.erase(pos);
cout << "删除" << x << "成功" << endl;
}
else
{
cout << x << "不在set中" << endl;
}
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}*/
if (s.count(5))
{
cout << "5在" << endl;
}
if (s.find(5) != s.end())
{
cout << "5在" << endl;
}
}
swap()
swap()就跟我们以前讲的一样,比如说你要交换两个set的对象,用算法的swap()直接交换是深拷贝,是树的拷贝,代价很大,所以set自己有一个swap(),它们交换指针,代价小很多。
count()
count()函数这个设计是和一个版本对应起来的,就是你给我一个值,我看一下这个值在我set里面有几个,在这里的set而言,无非就是1个和0个。但是待会儿还有一个对称的版本。我们说了,有些场景可能需要的不是排序➕去重,而只是排序,那么它们就设计了multile版本,它是多样性的意思,它是允许冗余的,它们的接口设计都是保持的一致。
》count()函数有一个非常有意义的点就是,判断在不在,比用find()方便。比如说,我要判断一个值在还是不在,用count()是不是就很方便呀,比如说我要count(3),if(s.count(3))看一下这个3在不在,在的话是会返回1,不在的话,是会返回0。如果用find()的话就没那么方便了吗,要这样if(s.find(3) != s.end())是不是相对来说不太方便了。
lower_bound()函数
lower_bound()是干嘛呢?比如lower_bound(30)是帮你去找 >= 30的值的迭代器,但是它在这儿又有一点问题,lower_bound()和upper_bound()的返回值还有点不一样。我们试验一下。
》你看set::iterator lowIt = s.lower_bound(3);如果有3,它会返回3位置的迭代器,没有就返回 > 3的值,我们插入的值里面是有3的,所以返回的是3 的迭代器。如果是set::iterator lowIt = s.lower_bound(6);我们插入里面没有6,那它会返回什么呢?返回的是7的迭代器。
》它什么情况下比较有用么,比如说:要求删除 >= x的所有值,那么你就不用遍历去删除了,这里的lower_bound()函数就可以帮干这件事情,怎么删除呢?不要忘了,我们的erase()是支持迭代器区间的删除!那么erase()左区间可以给个lowIt变量,右区间可以给end(),相当于是左闭右开【),即erase(lowIt,s.end());
void test_set4()
{
set<int> s;
s.insert(4);
s.insert(5);
s.insert(1);
s.insert(3);
s.insert(2);
s.insert(7);
s.insert(9);
// 返回>= val得位置迭代器 3返回3位置 6 返回7位置
/*set<int>::iterator lowIt = s.lower_bound(3); 存在
lowIt = s.lower_bound(6); 不存在*/
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 要求删除>=x的所有值
int x;
cin >> x;
set<int>::iterator lowIt = s.lower_bound(x);
s.erase(lowIt, s.end());
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
upper_bound()
比如upper_bound(60),如果有60的话,它是不回返回60,而是返回 > 60的值,我们来验证一下。这里我们比如说是,set::iterator upIt = s.upper_bound(5),我们传一个5,然后再来一个,set::iterator upIt = s.upper_bound(6),我们传的是6,这两个区别就是一个值在我们插入的里面,一个是不存在。我们来看看结果,我传的是5,有5它返回5了吗?答案是:不会的!它返回的是7,我传的是6,它返回的是7。所以,这里的upper_bound(),它要找的不是 >=的值,它是返回 > x位置的迭代器。
》所以upper_bound和lower_bound,一个是返回 >=,一个是返回 >。其实他们**两个可以配合起来并配合ease(),贯彻“左闭右开【)”。**比如说要删除 x <= [] <=y的区间,是不是就可以将这两个配合起来使用了呀。比如auto leftIt = s.lower_bound(x),auto rightIt = s.upper_bound(y),erase(letfIt,rightIt)所以它会删除【x,y】
void test_set5()
{
set<int> s;
s.insert(4);
s.insert(5);
s.insert(1);
s.insert(3);
s.insert(2);
s.insert(7);
s.insert(9);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 返回>x位置的迭代器 -》 都是返回 7位置的迭代器
//set<int>::iterator upIt = s.upper_bound(5); // 存在
//upIt = s.upper_bound(6); // 不存在
// 删除x <= <= y的区间 删除 [x,y]
int x, y;
cin >> x >> y;
auto leftIt = s.lower_bound(x); // [
auto rightIt = s.upper_bound(y); // )
s.erase(leftIt, rightIt);
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
讲讲multiset版本
multiset它是允许键值冗余的,它的用法和set一样的,但就是要记一下它的名字,比如mutilset mS;它是排序不去重。
》真正有区别的就是count()函数,比如说你要统计一个值s.count(1);set版本那边的count没有给bool类型的返回值,而是给int类型返回值,这其实是为保证接口的设计一致。其实set不需要count,是个bool就行了,但是为了和multiset保持一致,返回值类型也是int。
》同样的,set版本里面的erase()函数的返回值不是bool,而是size_type,也是为了和multiset里面的erase()设计保持一致,可以返回删除了几个对应的值。
》还有一个区别就是这里的find()函数,比如我这里有多个同样的值,那么我auto pos = s.find(3)会返回哪个3的迭代器呢?那么这里就可以做一件事情,它会返回中序的第一个,可以由while(pos != s.end())cout << *pos << " ";++pos验证。我们以后讲了搜索树,会知道,插入的时候,树是会旋转的,所以同样的值插入在左边还是右边都不影响。
void test_set6()
{
multiset<int> s;
s.insert(4);
s.insert(5);
s.insert(2);
s.insert(1);
s.insert(1);
s.insert(3);
s.insert(2);
s.insert(1);
s.insert(3);
s.insert(3);
s.insert(3);
s.insert(3);
// 排序
set<int>::iterator it = s.begin();
while (it != s.end())
{
//*it = 10;
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
cout << s.count(1) << endl;
cout << s.erase(1) << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 多个x的话,find返回中序第一个x
auto pos = s.find(3);
while (pos != s.end())
{
cout << *pos << " ";
++pos;
}
cout << endl;
}
看一下题目
题一:
两个数组的交集,找交集怎么找呢?是不是可以考虑这样,你也有,我也有的就是交集。
》最简单的方案就是,将其中一个数组放进set里面,我直接去你里面找,是不是太慢了。我们可以遍历将值inser()插入进set里面。然后继续遍历另一个数组,在我们的set里面找对应的值,那其中是不是就可以用count()函数而不必调用find()函数呀,然后,存在的话,就将值插入到我们另一个开辟的数组里面。但是这里有可能过不去,为什么呢?因为在我们开辟的vector来存交集的值,会有重复!因为交集是隐形的要求你去重的。
》那你的另一个数组是不是也用set来去重一下呀。但是这个算法不够高效,因为,假设你set里面有n个元素,那么每调用一次count()的时间复杂度是logN,那么总的时间复杂度是不是N*logN。
》还有一个优化算法,这个优化算法在很多找交集都能有效。找并集的话,只需要一个set就行了。现在来说一个交集/差集的优化算法,这个算法不一定要set,但是set用在这里刚刚好。
》我们inset()到set之后是不是就相当于有序了呀,有了排序就好说。如果你要求并集的话,只要放到一个set里面就行了,它是可以去重的。那么交集和差集怎么搞呢,有些地方需要同步,同步数据的话就要找交和差集。
》两个数组已经是有序了的,然后分别有一个指针先是指向各自的开始。交集的话:两个相比较的话,1.小的++;2.相等的就是交集,同时++;3.有一个结束就结束了。这个优化之后就变成了时间复杂度是2N了。
》这个算法除了可以找交集,还可以找差集。1.相比较,小的就是差集,小的++;2.相等的话,同时++;3.一个走到头了,另一个剩下的是不是也属于差集了。
》差集和交集在,服务器和你网盘需要同步的时候是有用到的。比如你有一个服务器文件夹和一个本地文件夹,你要将本地文件同步到服务器文件里面,相当于备份嘛,那是不是一方删除,另一方也会删除,你添加的,另一方是不是也要添加呀。
map
首先map数据结构里面呢,有一个Key和Value。它里面有一个valuse_type类型,它里面不是把数据分开存储的,不像我们前面讲的,自己写的搜索树里面,单独分开定一个key和value,而map,它是将key和value是放在一个结构里面的,这个结构是在C++里面被定义成pair<>类型,pair就是一对一对的意思,在map这里呢叫做键值对。
insert()
》我们先用map来存一个字典的例子,map的insert()插入的一个值是value_type类型的,value_type其实就是pair<>类型的数据。pair<>类型呢,有两个参数,第一个参数叫做first,第二个参数叫做second。
》如果我们想插入的话,map<string, string>dict;dict.insert(pair<string,string>(“sort”,“排序”));这是第一种,匿名对象的一种传参数,用起来比正常的要舒服很多。第二种,正常点是这样的,pair<string,string>kv(“insert”,“插入”);dict.insert(kv);还有第三种方式,用起来更爽:用到make_pair,make_pair就是制造一个pair对象,即dict.insert(make_pair(“left”,左边));
》前面第一、第二种都是借用的pair的构造函数来进行。第三种,make_pair是一个函数模版,它的优势是不用声明参数,让它自动推导。第四种,还能这么写,dict.insert({“right”,右边});虽然知道可以这么写,但是目前还是不太清楚为什么,这个涉及到C++11点类型转换,C++11再讲。
》map的遍历:map<string,string>::iterator it = dict.begin();我们也说过,如果你清楚的情况下可以用auto让它自己推导。while(it != dict.end())coyt << it << " ";++it;**发现不支持it**。有没有人想过,你直接分别单独用变量表示key和value不就好了嘛,为什么要封装一个pair?这个地方就能看出来,为什么要搞一个pair了。这个map的迭代器和链表的迭代器有些相似,它用自定义类型去封装,封装节点,只不过在map这里是一个树的节点,那么解引用是不是去调用operator呀,那C++是否支持一个函数返回两个值呢?这里it本质是it->operator(),它是不是要返回节点里面的数据呀,对于set还好,set里面只有一个key,但是map这里是一个key和value,那C++是否支持一次返回两个值呢?不支持是吧,但是你这里用到了模拟解引用的行为,那就要返回节点,如果你这里分别单独用变量表示key和value就没办法搞了,所以我把key和value给你打包成一个结构数据,这个结构数据就叫做pait<>类型,所以operator,返回的是节点数据,这个节点数据是一个pair<>类型。
》但是你这类型pair<>支不支持流呢,即cout << it吗?答案是:不支持。有同学说,我们来支持一下就可以了,但是支持的话是不是就要改库里面的代码呀,那么我们换一种方法玩。你不是一个结构嘛,那么我们就是cout << (*it).first << “:” << (*it).second << endl;但是在这种地方,你的迭代器是不是指针的行为呀,那你还用解引用*吗?不会,而是用迭代器重载的->运算符来用*,即cout << it->first << “:” << it->second << endl;
》那这里的范围for,for(const auto& kv : dict),kv其实是返回的*解引用后的pair数据类型,用kv.first()和kv.second()来访问。
void test_map1()
{
map<string, string> dict;
// pair构造函数
dict.insert(pair<string, string>("sort", "排序"));
pair<string, string> kv("insert", "插入");
dict.insert(kv);
// make_pair
auto ret1 = dict.insert(make_pair("left", "左边"));
auto ret2 = dict.insert(make_pair("left", "剩余"));
dict["operator"] = "重载"; // 插入+修改
dict["left"] = "左边、剩余"; // 修改
dict["erase"]; // 插入
cout << dict["left"] << endl; // 查找
// C++11 再讲
//dict.insert({ "right", "右边" });
// 遍历
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
while (it != dict.end())
{
//cout << *it << " "; // it->operator*()
//cout << (*it).first << ":" << (*it).second << endl;
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
for (const auto& kv : dict)
{
cout << kv.first << ":" << kv.second << endl;
}
}
//mapped_type& operator[] (const key_type& k)
//{
// pair<iterator,bool> ret = insert(make_pair(k, mapped_type()));
//
// //return (*(ret.first)).second;
// return ret.first->second;
//}
那你现在统计水果出现的次数是不是就很方便了呀,先构建一颗map树,map<string,int> countMap;for(aoto& str : arr)是不是就可以先调用map的find()函数,auto it = countMap.find(str);如果水果没有插入到我们的树里面,那么我们就构建一个pair<string,int>,即countMap.insert(make_pair(str,1));如果已经存在了,就it->second++;
void test_map2()
{
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
// 17:08继续
/*map<string, int> countMap;
for (auto& str : arr)
{
map<string, int>::iterator it = countMap.find(str);
if (it != countMap.end())
{
it->second++;
}
else
{
countMap.insert(make_pair(str, 1));
}
}*/
//map<string, int> countMap;
//for (auto& str : arr)
//{
// //pair<map<string, int>::iterator, bool> ret = countMap.insert(make_pair(str, 1));
// auto ret = countMap.insert(make_pair(str, 1));
// if (ret.second == false)
// {
// ret.first->second++;
// }
//}
map<string, int> countMap;
for (auto& str : arr)
{
countMap[str]++;
}
for (const auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
}
int main()
{
//test_set6();
test_map1();
return 0;
}
其实map这里最重要的就是刚刚讲的,那么刚刚上面讲的呢还有优化的空间,这个优化的空间是要帮我们去搞后面的事情。
map<string, int> countMap;
for (auto& str : arr)
{
map<string, int>::iterator it = countMap.find(str);
if (it != countMap.end())
{
it->second++;
}
else
{
countMap.insert(make_pair(str, 1));
}
}
这个代码其实是可以改进优化的。find()这里有这么一个问题,我去find()是不是一直去找,不在的话是不是我就相当于从根找到尾了呀,没有的话,我就得去插入,插入是不是又得去找一次,那是不是重复了呀,find()找一次,没找到后,insert又找了一次。其实不优化也无关痛痒,但是这里讲优化是为下面做铺垫,为下面的玩法做铺垫。
》所以这里就可以用insert()来做一件事情,正常的呢,insert()的返回值是一个pair<>类型的数据,pair的first数据被设置成为一个iterator迭代器,这个迭代器指向两种可能,要么指向新插入的元素(插入成功),要么指向与key相等的元素(插入不成功)。意味着是这样的,我insert()返回的pair<>类型的数据,first是一个迭代器,这个迭代器呢,一定是指向一个位置的,要么是你新插入元素的迭代器,要么是没有插入成功,因为反冗余的嘛,那么itreator就是已经有了的元素的迭代器。那么pair<>的second数据类型是一个bool,如果是新插入的,那就是true,如果已经有相等的了,就是flase。也就是说,它插入成功或者失败是会返回bool值的。我insert()返回的pair的迭代器不管你是插入成功还是失败都会返回一个元素的位置,那么就可以利用起来。
》就拿上面统计水果个树来说,如果水果已经存在map里面了,还会插入吗?不会。**这个待会儿对我们理解"[]"的使用非常重要 。**它insert()返回的pair类型的值非常有意义和我们待会儿要讲的“【】”有很大关系。我们插入atuo ret1 = dict.insert((“left”,左边))和auto ret2 = dict.insert((“left”,剩余)),按理来说第一次插入的left是成功的,ret1的second数据是true,first数据是新插入元素的迭代器;第二次插入left,ret2的second数据是flase,然后ret2的first数据和ret2一样的。
》我们就可以进行对上面代码改进,不需要再调用find()函数了,就调用insert()就行。我们不管有没有插入,都调用insert()函数,即pair<map<string,int>::iterator,bool> ret = countMap.insert(make_pair(str,1));写简单一点就是auto ret = countMap.insert(make_pair<str,1>);有两种情况,插入成功我用不用管,我不管,也就是第一次出现,那么返回的pair的second是一个true;插入失败,说明水果已经有了,那么insert()是不是同时充当了find()的作用,因为它把对应的元素的迭代器也给我了。 if(ret.second == false)那就是插入失败,已经有该水果了,所以ret.first->second++;
map<string, int> countMap;
for (auto& str : arr)
{
pair<map<string, int>::iterator, bool> ret = countMap.insert(make_pair(str, 1));
auto ret = countMap.insert(make_pair(str, 1));
if (ret.second == false)
{
ret.first->second++;
}
}
operator[]
》其实我们统计也不会用这个方法,这个方法太憋屈了,我们**讲这个真正的目的是为了玩“【】”**这个重载运算符,而且C++里面的map非常喜欢用“【】”,如果说map的话,我们重点学习的就是这个“【】”重载运算符。我们统计次数怎么统计,我先写好:
map<string, int> countMap;
for (auto& str : arr)
{
countMap[str]++;
}
此书成功统计出来,没有毛病,这个“【】”很绝。一句代码统计完次数!就用了operator []这么一个重载运算符。
》operator[]返回值是一个mapped_type&类型。我们想想,我们什么时候重载过operator[]了?我们在vector、string的时候重载了operator[],但是这里map支持随机访问吗?并不支持,目的也不是。
》其实这里map的operator[]已经改变了[]的意义了,它不是随机访问,它的参数也不是下标。它的参数是key,返回值是value的引用&。
》那么这里怎么做到一句话就统计了此书呢?insert()都没有了吗?我们来看看源码实现,
这个代码能把人看崩掉,我们简化一下代码,我们先提取出(this->insert(make_pair(K,mapped_type()))).first,这一串其实是谁呢?实际上就是调用了insert()嘛。
》insert的返回值是不是pair<map<>::iterator,bool>类型数据呀,即pair<map<>::iterator,bool> ret = insert(make_pair(K,mapped_type())),然后它取了ret的first数据,first是个什么呢,不就是指向节点的迭代器嘛。
》所以一开始的代码就可以理解成
pair<map<>::iterator,bool> ret = insert(make_pair(K,mapped_type()))
return (*(ret.first)).second;
》其实这个也绕了一点,其实等价于:
pair<map<>::iterator,bool> ret = insert(make_pair(K,mapped_type()))
return (ret.first)->second;
我们在“【】”里面给的是key值,有两种情况:1.Key在map对象中;2.Key不在map对象中。这里是精华,很多面试中都会考这里。
》第一种,Key在map对象中,我们要进行插入,make_pair(k,mapped_type())中的mapped_type()是个啥?我们先来看看mapped_type是谁?是不是T类型,就是Value的类型呀,也就是第二个模版参数的类型,第一个模版参数的类型是key的类型,K嘛。那么mapped_type()是不是用Value类型进行构造的匿名对象呀!
》也就是说,你来了一个Key,我不管你在不在,我先insert(0。假设第一种情况,Key是存在的,那么插入失败,那么insert()也就会插入失败,那么你pair<map<>::iterator,bool> ret = insert(make_pair(K,mapped_type()))中的ret的second数据bool类型就是flase!first数据就是Key所在节点的迭代器。然后return (*(ret.first)).second;可能早期没有支持“->”这个符号,所以我们现在支持,就这样写:ret.first->second------>这个是不是就是节点的Value值呀。并且不要忘了**,我operator[]的返回值,是Value的引用&!引用的作用是:1.减少拷贝;2.支持修改!**
》所以此时operato[]的作用有两层:1.查找K对应的Value;2.修改K对应的Value值。
》第一种情况,插入失败,insert()完全没有起作用,它反而充当的是查找的作用。它这里的operator[]返回值只用的是insert()返回值的first,即返回节点的迭代器。插入成功还是失败无所谓呀,为什么呢?如果Key在map对象中,肯定插入失败嘛;
》第二种情况,如果Key不在对象中,那么insert()是不是插入成功,operator[]的返回值是不是节点的迭代器中的Value值的引用&呀,这个值一开始是匿名构造的对象,上面有说(如果Value是stirng类型就是空串,如果是int类型就是0,如果是指针类型就是空nullptr)。我返回的Value值的引用,那也就意味着充当着 插入 和 修改两层作用,你可以修改,也可以不修改。
》我们再来分析一下,我们写的代码:
countMap[str]++;
水果要是第一次出现,那会先插入,插入的时候节点的个数是intt类型,它内部会匿名构造int类型对象就是0,但是operator[]会返回这个int的引用,我们在外面用了++,所以0会++;如果水果已经有了,它们operator[]不会插入成功,它会返回水果所在节点的Value值的引用,然后Value值++。
》我们再从另外一个角度,我们知道这个之后呢,有些人插入字典就不会像我们一开始那样写了,而是这样写:
auto ret1 = dict.insert(make_pair("left", "左边"));
auto ret2 = dict.insert(make_pair("left", "剩余"));
dict["operator"] = "重载"; // 插入+修改
dict["left"] = "左边、剩余"; // 查找 + 修改
dict["erase"]; // 插入
cout << dict["left"] << endl; // 查找
所以operator[]是一个多元化功能,map的其他函数参考set就行了。
multimap
它和map不同的是,它是允许冗余的,它是可以插入Key相同的键值对的,哪怕你的Value值相同也是可以的,其他的东西和map其实是一样的。
multimap<string,string> dict;
dict.insert(make_pair("left",“左边”));
dict.insert(make_pair("left",“剩余”));
》相比map ,multimap使用函数接口最大的区别是什么?multimap不支持operator[];为什么呢?如果对于map你给一个Key,我是不是要么有,要么没有,有的话,我就返回对应的Value,没有的话我好可以插入。但是mutiple有多个Key的时候,那么Key是不是面临一对多的关系呀,所以不支持operator []也是情理之中的。
题目:前K个高频单词
这个题目是一个统计次数➕topK的问题。我们不管三七二十一,先把各个单词出现的次数统计出来,当然可以用map来帮我们做KV映射,map<string, int> countMap;然后将words遍历一遍就可以得到各个单词出现的次数。
》接下来就是TopK的问题了。我们以前应该是讲过,如果建堆来解决,我们是不是要建小堆来解决是吧,但是我们不考虑用堆来解决,其实优先级也就是堆,因为优先级队列就是用堆来封装的。因为如果用堆的话,选出K个之后还需要进行排队。
》我们的map<string, int>它所插入的值,是按照string来进行排序的,但是我们要的是按int来进行排序。sort()能对map进行排序吗?答案是:不能!因为sort()要求的是随机迭代器,map显然不满足。那我们是不是要将map里面的值放到vector里面,然后就可以用sort()来进行排序了呀,当然vector存放的元素类型就是pair<string, int>了呀,即vrctor<pair<string, int>> vSort;
》当然也可以不放pair类型,而是vector<map<string, int>::iterator> v;这和我们放pair<string, int>有什么区别呢?这种方案行不行呢? 大家想想待会儿用sort()是不是必然要自己控制比较方式呀,如果要自己控制比较方式的话,你放pair<>我要写仿函数,我放迭代器你是不是也得控制比较方式呀。大家想一想,我在vector里面放迭代器iterator后,能不能访问到里面的Key和Value?是可以的。但是区别在哪里呢?如果你放的是pair<string, int>你待会儿得拷贝,把map里面的每一个数据都拷贝到pair里面,而我iterator迭代器就是一个指针,所以针对于拷贝的代价,我们还是选择迭代器iterator比较好。
》由于迭代器写出来很长,我们先typedef一下,typedef map<string, int>::iterator countIrer;然后我们就是遍历map,将每一个节点的iterator都插入到vector里面。
》接下来就是用到sort()函数进行排序了。 sort(v.begin(), v.end())这个时候能不能支持排序呢?是不能支持到,因为vector的迭代器v.begin()解引用之后是map节点的迭代器,那么它支持排序吗?不支持。不支持怎么办?我们是不是得写一个仿函数来控制排序使其支持呀。
》所以我们来写一个仿函数,struct IteCompare{}在其里面支持一个bool operator() (countIter ite1, countIter ite2)这里是支持迭代器进行比较。大家想清楚,因为vector里面的每一个数据是迭代器,我们将vector的迭代器传给sort,sort解引用*是map节点pair的迭代器,所以是迭代器比较;那么迭代器比较,要用仿函数来控制比较并使其能支持比较,所以我们写了bool operator() (countIter ite1, countIter ite2),然后函数体内写:return ite1->second > ite2->second;然后我们的仿函数就完成了。
》接下来就是将仿函数一并传给sort(),即sort(v.begin,v.end(),IterCompare);记住IterCompare是类型,我们应该传一个对象给sort(),所以是用到匿名对象IteComapre(),即sort(v.begin,v.end(),IterCompare());
》然后我们排序好了,我们还得取出前K个,是不是还得再来一个vector ret;然后将排完序后的vector循环K次呀,并且将值push_bacnk()进我们的ret里面。
》但是光光这样还是不能提交成功,因为我们的选出来的K个words单词顺序不对,因为如果单词出现的次数相同的话,还得进行ASICC码排序,将出现次数相同的单词进行排序。
》按道理,一开始我们的countMap是按string的大小(也就是按照Key去排序的)进行将每一个节点插入的,所以一开始就是将按string将words排序好了,然后紧接着我们再遍历将每一个节点的迭代器放入vector了呀,此时也是按照words的string来进行排序的,但是我们后面用了sort()之后,sort()是一个不稳定的排序,它会将我们原来的words正确顺序给打乱的。所以,你sort()完之后,选出来了K个,但是words的顺序你还得再排一次,除非你用一个稳定的排序。
》在sort()文档里面它有告诉你,它sort()是一个不稳定排序,它提供了一个stable_sort()是能够保证稳定性的。所以,我们将sort()换成stable_sort()试一下能不能通过,是能通过的。
》或者还有方法,你如果提前考虑到了这点的话,你可以在仿函数阶段也将words的顺序也控制好,即(ite1->second > ite2->second || ite1->second == ite2->second && ite1->first > ite2->first),所以我认为次数大的肯定大,如果次数相等,再比较words来进行排序。这样就可以调用sort()了。这不是改变sort()的稳定性,而是改变了比较的规则。但是会有一些性能的影响。
》我们还有别的方法,如果要排序,除了用sort(),是不是map本身也支持排序呀,只不过一开始是对string来排序的,我再定义一个map<int, string> sortMap;那么就是遍历一遍countMap,然后sortMap.insert(make_pair(kv.second,kv.string));就是将K和V调个位置,但是map会进行去重,所以我们还得用mutilmap才行,即mutilmap<int, string> sortMap;然后我们再去sortMap里面去取元素就行了,即for()循环K次,然后ret.push_back(it->second) ++it。但是取出来K个之后,它是按升序来的,显然不符合我们的要求,此时我们的map、mutilmap都是支持自己定义比较方式的,这不就体现出来了吗,所以,我们在定义sortMap的时候,还得传入另外一个参数,mutilmap<int, string, greater> sortMap;要传入一个greater来进行控制比较方式。
》最靠谱的还是sort或者srable_sort,因为如果用map的太依赖其底层的实现了。