【C++笔记】map和set的使用
【C++笔记】map和set的深度剖析

🔥个人主页:大白的编程日记
🔥专栏:C++笔记
 
文章目录
- 【C++笔记】map和set的深度剖析
- 前言
- 一.set
- 1.1 序列式容器和关联式容器
- 1.2 set系列的使用
- 1.3 set类的介绍
- 1.4 set的构造和迭代器
- 1.5 set的增删查
- 1.6 lower_bound和lower_bound
- 1.7 multiset和set的差异
- 1.8 set的应用
 
- 二. map系列的使用
- 2..1 map和multimap参考文档
- 2.2 map类的介绍
- 2.3 pair的介绍
- 2.4 pair使用
- 2.5 map的构造
- 2.6 map增删查
- 2.7 operator[]
- 2.8 multimap和map的差异
- 2.9 map的应用
 
- 后言
 
前言
哈喽,各位小伙伴大家好!上期我们讲了位图和布隆过滤器。今天我们来讲一下map和set的使用。话不多说,我们进入正题!向大厂冲锋
一.set
1.1 序列式容器和关联式容器
-  序列式容器 
 前面我们已经接触过STL中的部分容器如:string、vector、list、deque、array、forward_list等,这些容器统称为序列式容器,因为逻辑结构为线性序列的数据结构,两个位置存储的值之间⼀般没有紧密的关联关系,比如交换⼀下,他依旧是序列式容器。顺序容器中的元素是按他们在容器中的存储位置来顺序保存和访问的。
-  关联式容器 
 关联式容器也是用来存储数据的,与序列式容器不同的是,关联式容器逻辑结构通常是非线性结构,两个位置有紧密的关联关系,交换⼀下,他的存储结构就被破坏了。顺序容器中的元素是按关键字来保存和访问的。关联式容器有map/set系列和unordered_map/unordered_set系列。本章节讲解的map和set底层是红黑树,红黑树是⼀颗平衡二叉搜索树。set是key搜索场景的结构,map是key/value搜索场景的结构。
1.2 set系列的使用
set和multiset参考文档:
 set和multiset参考⽂档
 这里可以使用文档学习map和set.
1.3 set类的介绍
- 声明
 set的声明如下,T就是set底层关键字的类型
template < class T,                        // set::key_type/value_typeclass Compare = less<T>,        // set::key_compare/value_compareclass Alloc = allocator<T>      // set::allocator_type> class set;

-  仿函数 
 set默认要求T支持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数
-  空间配置器 
 set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。
-  模版参数 
 ⼀般情况下,我们都不需要传后两个模版参数。
-  底层结构 
 set底层是用红黑树实现,增删查效率是 ,迭代器遍历是走的搜索树的中序,所以是有序的。
1.4 set的构造和迭代器
set的构造我们关注以下几个接口即可。
 
set的支持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围for,set的iterator和const_iterator都不⽀持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。
// 迭代器是⼀个双向迭代器
iterator -> a bidirectional iterator to const value_type
// 正向迭代器
iterator begin();
iterator end();
// 反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();

1.5 set的增删查
-  insert 
 set的插入主要支持key关键字插入,迭代器位置插入,迭代器区间插入,initializer_list插入
   
 这里有两个2会插入失败。因为set不允许介质冗余。同时插入后的数据中序遍历就是有序的。所以set有排序和去重的功能。
 默认是升序。如果想控制升降序就用仿函数控制即可。
 如果是降序那就是大的数在左边,小的数在右边。
 中序遍历出来就是降序。
  
 initializer_list插入。
  
  
-  修改 
 set不允许修改。因为set底层是红黑树,也就是二叉平衡搜索树的变形。所以修改key会破坏树的性质。
  
-  find 
 set的查找支持key关键字查找。同时底层也是利用二叉搜索树性质查找。
 查找高度次。所以是O(logN).
 如果找到返回该位置的迭代器。
 如果没找到就返回end迭代器
  
 这里的value是为了和map对称实际还是key。
  
 也可以用count查找。返回key的个数。
-  erase 
 有三种方式:
 迭代器删除,key关键字删除,迭代器区间删除。
  
 因为默认是升序,同时迭代器走的是中序遍历。
 所以直接删除begin就是最小值。
  
  
 如果删除失败就返回end迭代器。
 按照key关键字删除
  
 返回删除个数也是为了和map对称。
注意因为删除后迭代器失效,所以erase返回迭代器的下一个位置。
- 迭代器失效
 set删除后迭代器失效。
  
  
 所以我们删除迭代器后就不要访问了。
 迭代器删除
  
1.6 lower_bound和lower_bound
-  lower_bound 
 返回大于等于val位置的迭代器 
-  upper_bound 
 返回大于val位置的迭代器。 
 这两个接口的作用在迭代器区间删除的时候,删除时迭代器区间默认都是左边右开
 这是我们就可以用lower_bound查找左端点,x存在就是x迭代器。不在就会找到大于x的第一个迭代器位置。upper_bound查找右端点的下一个位置。就可以删除形成某段特定值的左闭右开的区间了。
 底层查找也是利用二叉搜索树的性质查找。效率也很高。
  
1.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,9 };auto it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;// 相⽐set不同的是,x可能会存在多个,find查找中序的第⼀个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;// 相⽐set不同的是,erase给值时会删除所有的xs.erase(x);for (auto e : s){cout << e << " ";}cout << endl;return 0;
}

 multiset也排序但是不去重。
 这里multiset的查找和删除和set重点区别一下。
-  查找 
 找到中序的第一个。
  
  
-  删除 
 如果按照key关键字删除就把所有相同值的节点都删除。
  

 
 multiset和set的接口基本都一致。
1.8 set的应用
这里我们来做两个题体会一下set的使用场景。
-  题目一 
 两个数组的交集
  
-  思路分析 
  
-  同步算法 
  
-  代码实现 
class Solution {
public:vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {set<int> s1(nums1.begin(),nums1.end());set<int> s2(nums2.begin(),nums2.end());vector<int> ret;auto it1=s1.begin();auto it2=s2.begin();while(it1!=s1.end()&&it2!=s2.end()){if(*it1>*it2){it2++;}else if(*it1<*it2){it1++;}else{ret.push_back(*it1);it1++;it2++;}}return ret;}
};

-  题目二 
 环形链表2
  
-  思路分析 
  
-  代码实现 
class Solution {
public:ListNode *detectCycle(ListNode *head) {set<ListNode*> tmp;ListNode *cur=head;while(cur!=nullptr&&!tmp.count(cur)){tmp.insert(cur);cur=cur->next;}return cur;}
};

二. map系列的使用
2…1 map和multimap参考文档
这是map和multimap参考文档:
 map和multimap参考⽂档
2.2 map类的介绍
map的声明如下,Key就是map底层关键字的类型,T是map底层value的类型,set默认要求Key支持小于比较,如果不支持或者需要的话可以自行实现仿函数传给第⼆个模版参数,map底层存储数据的内存是从空间配置器申请的。⼀般情况下,我们都不需要传后两个模版参数。map底层是⽤红黑树实现,增删查改效率是O(logN) ,迭代器遍历是走的中序,所以是按key有序顺序遍历的。

template < class Key,                                     // map::key_typeclass T,                                       // map::mapped_typeclass Compare = less<Key>,                     // map::key_compareclass Alloc = allocator<pair<const Key,T> >    // map::allocator_type> class map;
2.3 pair的介绍
map底层的红黑树节点中的数据,使用pair<Key, T>存储键值对数据。
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){}
};


 pair是一个类模板。主要包含first和second两个成员。
 可以理解为pair就是个结构体。、
 所以我们现在就可以理解为之前我们是分散存放key和value。
 现在放在一个结构体里面。
 
- make_pair
 make_pair也是一个类模板。作用就是传两个参数,编译器推导出类型后,生成一个pair对象返回。 
2.4 pair使用
这里用pair写起来就比较方便。
#include<iostream>
#include<map>
using namespace std;
int main()
{// insert插⼊pair对象的4种⽅式,对⽐之下,最后⼀种最⽅便map<string, string> dict;pair<string, string> kv1("first", "第一个");dict.insert(kv1);dict.insert(pair<string, string>("second", "第二个"));dict.insert(make_pair("sort", "排序"));dict.insert({ "auto", "自动的" });// "left"已经存在,插⼊失败dict.insert({ "left", "左边,剩余" });while (it != dict.end()){//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;return 0;
}
同时这里遍历的时候,我们不能这样写。因为pair不支持流插入和流提取。
 
 所以我们可以这样显示的写出first和second
 
 本质是通过运算符重载找到pair*,在通过pair*访问first和second
 
 但是为了好看就省略了一个->.
 
 注意这里我们插入了第二个auto时,value也不会修改。
 插入相同的值就失败了。因为map不允许冗余。multimap才可以插入多个值。
2.5 map的构造
map的构造我们关注以下几个接口即可。
 map的支持正向和反向迭代遍历,遍历默认按key的升序顺序,因为底层是⼆叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围for,map支持修改value数据,不支持修改key数据,修改关键字数据,破坏了底层搜索树的结构。
 
// 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,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
// 迭代器是⼀个双向迭代器
iterator->a bidirectional iterator to const value_type
// 正向迭代器
iterator begin();
iterator end();
// 反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();

 构造时我们通常这样初始化,因为很方便。
 
2.6 map增删查
map的增删查关注以下几个接口即可:
 map增接口,插入的pair键值对数据,跟set所有不同,但是查和删的接口只用关键字key跟set是完全类似的,不过find返回iterator,不仅仅可以确认key在不在,还找到key映射的value,同时通过迭代还可以修改value
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);
// 返回⼤于等k位置的迭代器
iterator lower_bound(const key_type& k);
// 返回⼤于k位置的迭代器
const_iterator lower_bound(const key_type& k) const;
-  insert 
 insert插入一个pair的键值对
  
-  erase 
 map的erase只跟key有关。
  
-  find 
 map的查找也只跟key有关系,和value无关。
  
2.7 operator[]
这里我们快速统计水果出现的次数。
#include<iostream>
#include<map>
#include<string>
using namespace std;
int main(){// 利⽤find和iterator修改功能,统计⽔果出现的次数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;
}

 这里我们也可以用operator[]一行搞定。

 为什么呢?这就得好好研究operator[]的底层了。
这里的operator[]有三种功能:插入 查找 修改。
2.8 multimap和map的差异
multimap和map的使用基本完全类似,主要区别点在于multimap支持关键值key冗余,那么insert/find/count/erase都围绕着支持关键值key冗余有所差异,这里跟set和multiset完全⼀样,比如find时,有多个key,返回中序第⼀个。其次就是multimap不支持[],因为支持key冗余,[]就只能支持插入了,不能支持修改。
-  find 
 有相同查找中序的第一个。
-  insert 
 插入肯定成功,因为允许介质冗余。
-  erase 
 有相同值全部删除。
-  equal_range 
 这里equal_range作用主要是传一个key。
 那会就会返回一段迭代器区间。
 这段区间是所有key节点的左闭右开区间。
 左端点指向第一个key的迭代器,右端点指向第二个key的迭代器下一个位置。
  
-  operator[] 
 multimap不支持[].因为多个key,那该返回哪一个呢?
 所以不支持。
2.9 map的应用
-  题目一 
 随机链表的复制
  
-  思路分析 
  
-  代码实现 
class Solution {
public:Node* copyRandomList(Node* head) {map<Node*,Node*> hash;Node* phead=nullptr,*tail=nullptr;Node* pur=head;while(pur){Node* node=new Node(pur->val);if(phead){tail->next=node;tail=node;}else{tail=phead=node;}hash.insert({pur,node});//建立映射关系pur=pur->next;}pur=head;while(pur){hash[pur]->random=hash[pur->random];pur=pur->next;}return phead;}
};

-  题目二 
 前k个高频单词
  
-  思路分析 
  
  
-  代码实现 
stable_sort:
class Solution {
public:using  PSI=pair<string,int>;struct cmp{bool operator()(const PSI& a,const PSI& b)//比较{return a.second>b.second;}};vector<string> topKFrequent(vector<string>& words, int k) {map<string ,int> countmap;vector<string> ret;for(auto& x:words)//统计单词频次{countmap[x]++; }vector<PSI> v(countmap.begin(),countmap.end());stable_sort(v.begin(),v.end(),cmp());for(int i=0;i<k;i++)//提取结果{ret.push_back(v[i].first);}return ret;}
};
仿函数控制稳定:
class Solution {
public:using  PSI=pair<string,int>;struct cmp{bool operator()(const PSI& a,const PSI& b)//比较{return a.second>b.second||(a.second==b.second&&a.first<b.first);}};vector<string> topKFrequent(vector<string>& words, int k) {map<string ,int> countmap;vector<string> ret;for(auto& x:words)//统计单词频次{countmap[x]++; }vector<PSI> v(countmap.begin(),countmap.end());sort(v.begin(),v.end(),cmp());for(int i=0;i<k;i++)//提取结果{ret.push_back(v[i].first);}return ret;}
};
堆top-k:
class Solution {
public:using  PSI=pair<string,int>;struct cmp{bool operator()(const PSI& a,const PSI& b)//比较{return a.second<b.second||(a.second==b.second&&a.first>b.first);}};vector<string> topKFrequent(vector<string>& words, int k) {vector<string> ret(k);unordered_map<string ,int> hash;for(auto& x:words)//统计单词频次{hash[x]++;}priority_queue<PSI,vector<PSI>,cmp> heap(hash.begin(),hash.end());for(int i=0;i<k;i++)//提取结果{ret[i]=heap.top().first;heap.pop();}return ret;}
};

后言
这就是C++三大特性之继承。大家自己好好消化!今天就分享到这!感谢各位的耐心垂阅!咱们下期见!拜拜~


