一、序列式容器和关联式容器
前⾯我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,⽐如交换⼀下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。
关联式容器也是⽤来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是⾮线性结构,两个位置有紧密的关联关系,交换⼀下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。
本章节讲解的map和set底层是红⿊树,红⿊树是⼀颗平衡⼆叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构注意说明: key就是比如我们的上篇文章的二叉搜索树的数据val,就是二叉搜索树的平衡是靠key来维护的,故此这里的set和map的底层是红黑树,红黑树的底层是平衡二叉搜索树,我们map和set所讲的key也就是维护平衡的关键词,value就是我们想要输出的值,当然很多情况key也要输出
二、set的介绍
set的相关文档: https://legacy.cplusplus.com/reference/set/set/?kw=set
2.1 set的性质
set的声明如下,T就是set底层关键字的类型:
1.set默认要求T⽀持⼩于⽐较,如果不⽀持或者想按⾃⼰的需求⾛可以⾃⾏实现仿函数传给第⼆个模版参数
2.set底层存储数据的内存是从空间配置器申请的,如果需要可以⾃⼰实现内存池,传给第三个参数。
3. ⼀般情况下,我们都不需要传后两个模版参数。
set底层是⽤红⿊树实现,增删查效率是
O(logN),迭代器遍历是⾛的搜索树的中序,所以是有序的2.2 set的常用使用接口
set的构造
根据文档来看:
set的中重要的构造就是迭代器区间构造和支持在创建对象时支持花括号构造
cpp
set<int> s = { 5,8,9,2,3,4,6,9,10 };
set<int> s1(s.begin(), s.end());
如上的代码就是创建set对象常用的方法 ,其实这两种方法在我们之前学的容器都能这样,这里就不在说明
set的迭代器
set的⽀持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是⼆叉搜索树,迭代器遍历⾛的中序;⽀持迭代器就意味着⽀持范围for,set的iterator和const_iterator都不⽀持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。
cpp
auto it = s.begin();
while (it != s.end())
{
*it=100;//运行直接报错,不允许改key的值
cout << *it << ' ';
++it;
}`
这里的key的值不能修改。改了就破环了结构,到后面我讲的map的key也不能改,但是value能改,可以说set这里的key和value时同一个值
set的插入
size_type-> size_t
value_type-> T
cpp
size_type insert(const value_type& val);
iterator insert(iterator first,iterator last);
一般就为这两个用的最多
cpp
set<int> s = { 5,8,9,2,3,4,6,9,10 };
s.insert(100);
for (auto& e : s)
{
cout << e << ' ';
}
set的删除
cpp
// 删除⼀个迭代器位置的值
iterator erase (const_iterator position);
// 删除val,val不存在返回0,存在返回1
size_type erase (const value_type& val);
// 删除⼀段迭代器区间的值
iterator erase (const_iterator first, const_iterator last);
set的查找函数与计数函数
cpp
// 查找val,返回val所在的迭代器,没有找到返回end()
iterator find (const value_type& val);
// 查找val,返回Val的个数
size_type count (const value_type& val) const;
注意:只要是有迭代器区间的插入删除或查找等函数都是[first,last)在这样的左闭右开的区间进行的操作
其他函数
cpp
// 返回⼤于等于val位置的迭代器
iterator lower_bound (const value_type& val) const;
// 返回⼤于val位置的迭代器
iterator upper_bound (const value_type& val) const;
三、multiset的使用
基本接口于set相同不同的是set里不允许右重复的数据,而multiset可以有重复的数据,这也是count有返回值的原因,但set有的返回值的原因是保持接口性一致
这里就不展开讲因为,增删查改和构造的接口基本一致,就是可以允许有数据重复
力扣题目的运用set
力扣349题: 两个数组的交集
cpp
set<int> s1(nums1.begin(),nums1.end());
set<int> s2(nums2.begin(),nums2.end());
vector<int> v;
for(auto& e: s1)
{
if(s2.count(e))
{
v.push_back(e);
}
}
return v;
}
思路就是利用set的默认去重,遍历其中一个set,如果在里面找到了那就是它们的交集,就插入到要返回的数组里,这里用find也行,count计数更方便,因为找到了就返回当前数的个数,存在就至少为1就为真,就是交集
力扣142:环形链表||
cpp
ListNode *detectCycle(ListNode *head) {
set<ListNode*> s;
ListNode* cur=head;
while(cur)
{
if(s.find(cur)!=s.end())
{
return cur;
}
s.insert(cur);
cur=cur->next;
}
return NULL;
}
<font size="4" color="b">思路讲解就是遍历整个链表,将所有链表的结点输入到set中,如果该结点在set中找到了就证明是带环的链表,并且该结点就是入环结点,没找到那就插入就行,直到遍历完链表时,那就是不带环的链表 ,注意这里set存的是结点的指针地址,所以能通过find函数查找< /font>
四、map的使用
map的文档:map的相关文档
4.1map的介绍
map的声明如下,Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key⽀持⼩于⽐较,如果不⽀持或者需要的话可以⾃⾏实现仿函数传给第⼆个模版参数,map底层存储数据的内存是从空间配置器申请的。⼀般情况下,我们都不需要传后两个模版参数。map底层是⽤红⿊树实现,增删查改效率是,迭代器遍历是⾛的中序,所以是按key有序顺序遍历的。
4.2pair类型的介绍
map底层的红⿊树节点中的数据,使⽤pair<Key,T>存储键值对数据。
相较于set,map多了一个参数value,所以它里面存的值由 key和value,因为输出数据的时候一般把key和value都输出,但是流输出cout只能输出一个值,所以把这两个值封装在一个类pair中,里面的成员就是key和value
cpp
typedef pair<const Key, T> value_type;
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)
{}
};
template<class U, class V>
pair (const pair<U,V>& pr): first(pr.first), second(pr.second)
{}
template <class T1,class T2>
inline pair<T1,T2> make_pair (T1 x, T2 y)
{
return ( pair<T1,T2>(x,y) );
}
4.1map的构造
map的构造和set类似我们关注以下⼏个接⼝即可。
map的⽀持正向和反向迭代遍历,遍历默认按key的升序顺序,因为底层是⼆叉搜索树,迭代器遍历⾛的中序;⽀持迭代器就意味着⽀持范围for,map⽀持修改value数据,不⽀持修改key数据,修改关键字数据,破坏了底层搜索树的结构。
cpp
// empty (1)
⽆参默认构造
explicit map (const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
// range (2) 迭代器区间构造
template <class InputIterator>
map (InputIterator first, InputIterator last,
const key_compare& comp = key_compare(),
const allocator_type& = allocator_type());
// copy (3) 拷⻉构造
map (const map& x);
// initializer list (5) initializer 列表构造
map (initializer_list<value_type> il,
cpp
pair<string, string> kv1("const", "常量属性");
pair<string, string> kv2("wait", "等待");
pair<string, string> kv3("continue", "持续");
map<string, string> m1 = { kv1,kv2,kv3 };
map<string, string> m3 = { {"const", "常量属性"},{"wait", "等待"},{"continue", "持续"} };
比如实现单词的查找,就可以把英文当作key,中文翻译当作value,如上的构造方法是常用的
4.2map的增删查的函数
map增接⼝,插⼊的pair键值对数据,跟set所有不同,但是查和删的接⼝只⽤关键字key跟set是完全类似的,不过find返回iterator,不仅仅可以确认key在不在,还找到key映射的value,同时通过迭代器还可以修改value
cpp
Member types
key_type -> The first template parameter (Key)
mapped_type -> The second template parameter (T)
value_type -> pair<const key_type,mapped_type>
// 单个数据插⼊,如果已经key存在则插⼊失败,key存在相等value不相等也会插⼊失败
pair<iterator,bool> insert (const value_type& val);
// 列表插⼊,已经在容器中存在的值不会插⼊
void insert (initializer_list<value_type> il);
// 迭代器区间插⼊,已经在容器中存在的值不会插⼊
template <class InputIterator>
void insert (InputIterator first, InputIterator last);
// 查找k,返回k所在的迭代器,没有找到返回end()
iterator find (const key_type& k);
// 查找k,返回k的个数
size_type count (const key_type& k) const;
// 删除⼀个迭代器位置的值
iterator erase (const_iterator position);
// 删除k,k存在返回0,不存在返回1
size_type erase (const key_type& k);
// 删除⼀段迭代器区间的值
iterator erase (const_iterator first, const_iterator last);
4.3map的修改
由set我们可知key的值不能改,但是在map中,key的值依然不能改,但是value的值能改
cpp
map<string, string> m3 = { {"const", "常量属性"},{"wait", "等待"},{"continue", "持续"} };
m1.insert({ "love","爱" });
auto it = m3.begin();
while (it != m3.end())
{
/*(*it).first += 'x';*//修改会报错
(*it).second += 'x';
cout << (*it).first <<':'<< (*it).second << ' ' << endl;
++it;
}
4.4map的\[\]的使用
mapped_type-> value_type也就是pair的第二个参数value
cpp
mapped_type& operator[] (const key_type& k);
mapped_type& operator[] (key_type&& k);
这个方括号的重载底层是复用了insert函数
返回值如上,首先make_pair返回的是pair的对象,insert插入这个pair对象有个规则:1.如果里面插入的Key里面没有,那就会插入进去并返回当前的迭代器
2.如果里面存在那就是返回存在的与pair对象相同的key的迭代器
insert返回的对象是pair<iterator,bool>.first就是key的迭代器,解引用就是存储key的pair对象在.second那就是value值,引用返回就是当前key的value那就可以修改它,使用用途比如:统计一个单词在文章的出现次数
4.5multimap的使用
multimap和map的使⽤基本完全类似,主要区别点在于multimap⽀持关键值key冗余,那么insert/find/count/erase都围绕着⽀持关键值key冗余有所差异,这⾥跟set和multiset完全⼀样,⽐如find时,有多个key,返回中序第⼀个。其次就是multimap不⽀持\[\],因为⽀持key冗余,\[\]就只能⽀持插⼊了,不能⽀持修改。
4.6map在题目的使用场景
cpp
Node* copyRandomList(Node* head) {
if (!head) return nullptr;
map<Node*,Node*> m;
Node* copyhead=nullptr;
Node* copyptail=nullptr;
Node* cur=head;
while(cur)
{
if(copyhead==nullptr)
{
copyhead=new Node(cur->val);
copyptail=copyhead;
}
else{
copyptail->next=new Node(cur->val);
copyptail=copyptail->next;
}
m[cur]=copyptail;
cur=cur->next;
}
cur=head;
Node* copy=copyhead;
while(cur)
{
if(cur->random==nullptr)
{
copy->random=nullptr;
}
else
copy->random= m[cur->random];
cur=cur->next;
copy=copy->next;
}
return copyhead;
}
数据结构初阶阶段,为了控制随机指针,我们将拷⻉结点链接在原节点的后⾯解决,后⾯拷⻉节点还得解下来链接,⾮常⿇烦。这⾥我们直接让{原结点,拷⻉结点}建⽴映射关系放到map中,控制随机指针会⾮常简单⽅便,这⾥体现了map在解决⼀些问题时的价值,完全是降维打击。
cpp
struct compare {
bool operator()(pair<string, int> kv1, pair<string, int> kv2) {
return kv1.second > kv2.second ||
(kv1.second == kv2.second && kv1.first < kv2.first);
}
};
vector<string> topKFrequent(vector<string>& words, int k) {
map<string, int> countmap;
// 统计每个不同单词出现的次数
for (auto& e : words) {
countmap[e]++;
}
// 开始取前k个高频单词
vector<pair<string, int>> v(countmap.begin(), countmap.end());
sort(v.begin(), v.end(),
compare()); // 如果直接这样比较默认升序排列,所以要自己实现用仿函数降序
vector<string> ret;
for (int i = 0; i < k; ++i) {
ret.push_back(v[i].first);
}
return ret;
}
这道题也体现了仿函数的作用,因为sort排序是的底层是快排,不是稳定类型的排序,故当出现频率相同时,可能不是按字典顺序排列相应的单词顺序,故要自己写仿函数控制



