map和set是c++中最重要的容器之二。
最基本的认知:之前提到过,map和set是平衡二叉搜索树,set是K模型,map是KV模型。
先来看看set

包在头文件<set>中
set是一个模板,T就是key,compare则是比较方式,如果默认的less<T>不符合要求,比如指针,要求按指向数据的大小比,而不是按指针地址比,就需要自己写比较方式。
构造函数最常用的是无参的构造函数
数据的插入只有insert函数,其中value_type是第一个模板参数。线性数据结构的插入都是push,非线性数据结构则是insert。


由insert的返回值可知,set数据结构是有迭代器的,pair则是一个类模板,iterator和bool作为参数类型。pair具体是什么先不管,来看看set迭代器的情况:

set的迭代器是双向迭代器。来看看实际效果:

已知set是一颗搜索二叉树,迭代器遍历的结果呈升序排列(兼顾去重功能),可以确定,迭代器遍历的顺序就是搜索二叉树的中序遍历。
而且set迭代器的底层的循环迭代没有用栈实现,而是使用了一种特殊的算法,是搜索二叉树的第二种非递归遍历方式(前提是要有三叉链)。
set的迭代器还有一点不同与之前的数据结构:不支持对set中数据的修改,查看set源码可以看到iterator和const_iterator是同一个类型typedef出来的。

关于迭代器的其他部分就和之前的数据结构相同了,比如反向迭代器,比如范围for。
insert成员函数只有第一个经常用到,第二个是在指定位置插入数据,要慎用,因为自己指定位置插入可能会破坏搜索二叉树的结构,这种情况下,要么阻止插入数据,要么还是按没有指定位置的方式插入数据。

insert是一个节点一个节点插入数据的,插入数据不会发生像vector那样的数据挪动,所以不会发生迭代器失效的问题。
真正会导致迭代器失效(变成野指针)的是erase。erase常用的是前两个函数。

erase一般配合find使用,如果要删除的是set中没有的内容,erase也不会报错。find返回的类型是迭代器。从返回结果来看,如果找到,就返回该位置的迭代器,没有就返回set::end。


算法中也有一个find函数,和set中find的区别是什么?
算法中的find是暴力迭代查找,时间复杂度是O(N),而set中的find是搜索树查找,时间复杂度是O(logN)。
erase的返回值是size_type,是一个size_t类型,erase返回值不是bool,而是size_t类型的原因是要和允许出现冗余的multiset版本对应,返回值是删除元素的个数(set中返回值只有0和1,multiset则可能删除多个)。

count的作用就是看val在容器中的有多少个。
count比较重要的一个作用就是判断某个元素在不在比find方便。


lower_bound的作用是返回首个不小于val元素的位置(即迭代器),也就是说lower_bound返回首个>=val元素的位置。

lower_bound和erase组合可以完成对>=val区间的删除。

upper_bound的作用是返回首个大于val元素的位置,也就是说upper_bound返回首个>val元素的位置。

upper_bound和lower_bound组合使用,这两个函数这样设计的原因是贯彻迭代器的左闭右开。比如说要删除一个大于等于x,小于等于y的区间( [x, y] ),要把y删除,就要找比y大的位置。

如果输入的值在set中没找到对应的位置(即输入一个比set中所有值都大的val),则返回set::end。
multiset和set一样,存储在<set>中。
multiset和set相比,允许数据冗余,如果说set的功能是去重+排序,multiset的功能就是排序;其函数接口大部分与set功能相同,少部分有所不同,比如count,erase。find也有所不同,如果存在多个val,multiset的find会返回中序的第一个val。

这题用set来解决就是将num1和num2分别传到set中,然后遍历一个set,在遍历的set中用count在另一个set中找相同的元素,如果相同就存入输出数组中。不过set遍历自身是O(N),每次count是O(logN),这里的复杂度是O(N*logN),可以优化为O(N),(虽然总体复杂度还是O(N*logN),但优化后的算法在很多地方都会用到)
以[1,3,4,5,7,9]和[2,3,5,7,8,9]为例。找两个数组的交集,并集,差集
交集最简单,将两个数组存入set就可以。
并集:用两个迭代器指向数组的开头,比较指向值的大小,值小的迭代器++,值相等的就是交集,同时++。优化为O(N)。
差集:用两个迭代器指向数组的开头,比较指向值的大小,值小的就是差集,迭代器++,值相等的,迭代器同时++。一组走完,另一组剩下的全是差集。
交集和差集的内容在数据同步的地方用到很多。
下面来了解一下map
map存储在头文件<map>中,是二叉搜索树中的KV模型。

key就是key,T则是value。但是map不是按照我们写搜索树时将key和value作成员变量的,而是将key和value存入一个结构中。这个结构是c++官方库中定义的,叫pair,就是set中insert的返回值类型。

首先来看看map的insert

insert函数的参数是一个value_type类型,value_type是一个pair。

所谓的pair是库中定义好的一个类模板,有两个成员参数。

更直观一点看:

map的插入:

map的头文件中包了pair,make_pair的头文件,可以直接使用。

统计元素个数:

在这个统计元素个数的算法中有一个重复行为,find到搜索二叉树中找元素,如果没有找到就插入元素,但插入元素需要重新找到插入的位置,但这个位置之前find已经找到了。现在有没有办法将find找到位置利用起来?
这就要说到到insert了:


insert的返回值是一个pair,pair的第一个成员是一个迭代器,指向新插入的元素,如果没有插入成功就指向节点中的key与插入val中的first相同的节点。插入的成功与否,存储在pair的第二个成员中。如图所示:

可以根据这点设计优化算法:

不过实际统计次数的时候也不会这样使用(说一下是为了给operator[]作铺垫),而是通过operator[]来统计:

下面来了解一下operator[]:


简单来说operator[]就是在map中搜索对应的key(Key),如果找到,就返回value(T)的引用。这里重载的[]已经不是[]原来的作用了,map并不支持随机访问。
框出部分的含义是:调用这个函数等价于使用如下代码: (*((this->insert(make_pair(k,mapped_type()))).first)).second
代码拆解成便于理解的形式:
mapped_type& operator[] (const key_type& k)
{
auto pa = make_pair(k,mapped_type());
//mapped_type()是第二个模板参数,内置类型也可能是模板参数。
auto it = (this->insert(pa)).first;
//first是k所在节点的迭代器
//this->可以省略,operator[]作为map的成员函数,调用insert默认是map的insert。
return (*(it)).second;
//对迭代器解引用得到节点中的pair,再拿到pair的second,等价于it->second,second即value。
}
如果要插入k,会分成两种情况。1.插入成功,map中没有key,value值为默认构造生成的。作为引用返回。2.插入失败,map中有key,value值不变,作为引用返回。
所以之前countMap[e]++;就是引用返回了countMap的value,再++达到统计个数的效果。
这里operator[]的意义有两层:
key在map中,operator[]的作用是:1.查找key对应的value;2.修改key对应的value(引用返回)。
key不在map中,operator[]的作用是:1.插入key和value;2.修改key对应的value(引用返回)。
operator[]因此具备相当多的功能,以之前的字典为例:
dict["insert"] = "插入";
//插入+修改
dict["left"] = "左边,剩余";
//修改
dict["right"] ;
//插入
cout << dict["left"] << endl;
//查找
multimap的使用和map大致相同,但因为multimap的key会对应多个value,所以multimap没有operator[]函数,这也是map和multimap最大的区别,还有一些区别,比如insert的返回值,与map不同,multimap的插入是必定成功的,所以multimap插入函数的返回值是iterator。

最后以一道题收尾:

class Solution
{
public:
typedef map<string, int>::iterator iterMap;
class compareIterator
{
public:
bool operator()(iterMap it1, iterMap it2)
{
return it1->second > it2->second ||
(it1->second == it2->second && it1->first < it2->first);
}
};
vector<string> topKFrequent(vector<string>& words, int k)
{
map<string, int> countMap;
vector<string> ret;
for (auto& e : words)
{
countMap[e]++;
}
//下一步一般会想建堆来找出前topK个,但是这里还要求要按单词出现频率排序,相同频率还要按字典序排序
//虽然也能实现,但是太麻烦了。顺便说一下,找前k个大的建小堆,找前k个小的建大堆,因为是用前k个数建堆
//然后遍历判断是否进入堆,而是不是全部建堆,然后erase k次。
vector<iterMap> v;
//这里将迭代器存入vector是因为sort不能直接对map排序,因为map不支持随机访问(比较方式的问题写个仿函数就行)
//同样是存储countMap中的内容,迭代器相比于pair<string,int>小的多。
iterMap it = countMap.begin();
while (it != countMap.end())
{
v.push_back(it);
it++;
}
sort(v.begin(), v.end(), compareIterator());
//迭代器不支持比较,需要写仿函数确定比较规则。
for (int i = 0; i < k; i++)
{
ret.push_back(v[i]->first);
}
return ret;
}
};
这题还有一些展开:比如仿函数只写了return it1->second > it2->second; 没写后面个数相同时的比较方式,就无法通过测试。因为有相同次数的单词按字典序排序的要求,但之前存入map中时,已经按key排好序了,就是字典序,因为sort是不稳定排序,所以没有通过,如果按照稳定排序的方式排序,就能通过,稳定排序的方式库中也有,和sort的参数一样:

排序的方式除了sort还有map,以出现次数为key,对应单词为value存入map,考虑到存在次数相同的单词,所以用multimap。multimap默认是升序,要取前K个出现次数大的单词,就需要存入multimap时按降序(传greater<int>)排序。但这样存在隐患,这里multimap和greater<int>比较时是以一个稳定的方式比较,即相同时没有破坏相对顺序,但是在别的环境中可能以破坏相对顺序的方式比较,也就是说在其他环境下,相同的代码可能出错。