《map和set的使用介绍》
引言:
上次我们学习了第一个高阶数据结构—二叉搜索树,趁热打铁,今天我们就再来学习两个数据结构—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
的参考文档:
set / multiset的参考文档:
2. set
类的介绍:
set
的声明如下,T
就是set
底层关键字的类型
set
默认要求T
⽀持小于比较,如果不支持或者想按自己的需求走可以自行实现仿函数传给第二个模版参数。
2.·set
底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。- 一般情况下,我们都不需要传后两个模版参数。
set
底层是用红黑树实现,增删查效率是O(logN)
,迭代器遍历是走的搜索树的中序,所以是有序的。- 前面部分我们已经学习了
vector
/list
等容器的使用,STL容器接口设计,高度相似,所以这里我们就不再一个接口一个接口的介绍,只是挑比较重要的接口进行介绍。
template < class T,
class Compare = less<T>,
class Alloc = allocator<T> > class set;
3. set的构造和迭代器
(1)构造函数:
介绍文档:
set
的构造我们关注以下几个接口即可。
set
支持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序,支持迭代器就意味着支持范围for,set
的iterator
和const_iterator
都不支持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。
代码演示:
(1) 无参默认构造
explicit set (const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
(2) 迭代器区间构造
template <class InputIterator>
set (InputIterator first, InputIterator last,
const key_compare& comp = key_compare(),
const allocator_type& = allocator_type());
(3) 拷贝构造
set (const set& x);
(5) initializer 列表构造
set (initializer_list<value_type> il,
const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
(2) 迭代器:
begin():返回指向第一个数据的迭代器。
end():返回最后一个数据下一个位置的迭代器。
cbegin():返回最后一个元素的迭代器。
cend():返回第一个数据的前一个位置的迭代器。
set
的迭代器是一个双向迭代器
iterator -> a bidirectional iterator to const value_type
- 正向迭代器
iterator begin();
iterator end();
- 反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();
4. set
的增删查:
(1)插入:
insert函数
代码演示:
(2)查找:
- find(): 查找val,返回val所在的迭代器,没有找到返回end()。
- count():查找val,返回Val的个数。
代码演示:
(3)删除:
- iterator erase (const_iterator position): 删除一个迭代器位置的值。
- size_type erase (const value_type& val):删除val,不存在返回0,存在返回1。
- iterator erase (const_iterator first, const_iterator last):删除一段迭代器区间的值。
代码演示:
(4) lower_bound
和 upper_bound
- lower_bound:返回大于等val位置的迭代器。
- upper_bound:返回大于val位置的迭代器。
注:因此借助这两个接口就可以得到一段左闭右开的区间。
代码演示:
5. insert 和迭代器遍历使用样例:
注:这里的范围for我们写成了引用的形式,是为了提高效率。
6. find和erase的使用样例:
注:这是我们借助count间接实现的查找。
算法库中的find
和set
中的find
对比:
7. multiset
和set
的差异
multiset
和set
的使用基本完全类似,主要区别点在于multiset
支持值冗余,那么
insert
/find
/count
/erase
都围绕着支持值冗余有所差异,具体参看下面的样例代码理解:
8. 牛刀小试:
(1)题目描述:
代码解决:
方法一-暴力匹配:
先将两个数组通过set来去重,之后遍历其中一个来借助count来判断是否有交集。
方法二-双指针:
该解法的思路是在去重+升序排序后的基础上来遍历两个容器
定义两个指针p1
,p2
分别遍历两个set
- 小的那个
++
。 - 相等的话就存下来,
p1++
,p2++
。 - 当其中一个容器遍历完之后,就结束、
为什么这样是对的呢?
因为如果其中一个小的话,由于数据是升序的,因此当前必不可能为交集,若存在交集的话只可能在它之后,因此往后走。
题目传送门:349. 两个数组的交集
三: map
系列的使用
1. map
和multimap
的介绍文档:
map 和multimap的介绍文档:
2. map类的介绍:
map
的声明如下,Key
就是map
底层关键字的类型,T
是map
底层value
的类型,map
默认要求Key
支持小于比较,如果不支持或者需要的话可以自行实现仿函数传给第二个模版参数,map
底层存储数据的内存是从空间配置器申请的。一般情况下,我们都不需要传后两个模版参数。map
底层是用红黑树实现,增删查改效率是,迭代器遍历是走的中序,所以是按key
有序顺序遍历的。O(logN)
,迭代器遍历是走的中序,所以是按key
有序顺序遍历的。
template < class Key,
class T,
class Compare = less<Key>,
class Alloc = allocator<pair<const Key,T> > > class map;
3. pair类型介绍:
map
底层的红黑树节点中的数据,使用pair<Key,T>
存储键值对数据。
pair:
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) );
}
上面是pair
的底层结构,实质上就是一个类模版的结构体。先这么理解即可。
思考:为什么map
里面要用pair
呢?
这是因为map
里面是存储两个数据,如果你单独分开存两个数据的话,是没办法解引用的,因为解引用只能拿到一个数据,用pair
的话,解引用会拿到pair
类型的数据,之后可以通过pair
类型的性质来拿到两个数据。
4. map
的构造和迭代器:
(1) 构造函数:
map的构造函数介绍文档:
map
的构造我们关注以下几个接口即可。
map
的支持正向和反向迭代遍历,遍历默认按key
的升序顺序,因为底层是二叉搜索树,迭代器遍历走的中序;支持迭代器就意味着支持范围for,map
支持修改value
数据,不支持修改key
数据,修改关键字数据,破坏了底层搜索树的结构。
(1) 无参默认构造
explicit map (const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());
(2) 迭代器区间构造
template <class InputIterator>
map (InputIterator first, InputIterator last,
const key_compare& comp = key_compare(),
const allocator_type& = allocator_type());
(3) 拷贝构造
map (const map& x);
(4) initializer 列表构造
map (initializer_list<value_type> il,
(2)迭代器:
- begin():返回指向第一个数据的迭代器。
- end():返回最后一个数据下一个位置的迭代器。
- cbegin():返回最后一个元素的迭代器。
- cend():返回第一个数据的前一个位置的迭代器。
map
的迭代器也是⼀个双向迭代器
iterator -> a bidirectional iterator to const value_type
- 正向迭代器
iterator begin();
iterator end();
- 反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();
5. map 的增删查
(1)插入:
insert介绍文档:
代码演示:
(2)查找:
- find():查找k,返回k所在的迭代器,没有找到返回end()。
- count():查找k,返回k的个数。
代码演示:
(3) 删除:
- iterator erase (const_iterator position):删除⼀个迭代器位置的值。
- size_type erase (const key_type& k):删除k,k存在返回0,存在返回1。
- iterator erase (const_iterator first, const_iterator last:删除一段迭代器区间的值。
(4) lower_bound
和 upper_bound
- lower_bound:返回大于等k位置的迭代器。
- upper_bound:返回大于k位置的迭代器。
代码演示:
注:这里默认是按照第一个关键字来比较的。
6. map
的数据修改
前面提到map
支持修改mapped_type数据,不支持持修改key
数据,修改关键字数据,破坏了底层搜索树的结构。
map
第一个支持修改的方式是通过迭代器,迭代器遍历时或者find
返回key
所在的iterator
修改,map
还有一个非常重要的修改接口operator[]
,但是operator[]
不仅仅支持修改,还支持插入数据和查找数据,所以他是一个多功能复合接口
需要注意从内部实现角度,map
这里把我们传统说的value
值,给的是T
类型
typedef
为mapped_type
。而value_type
是红黑树结点中存储的pair
键值对值。日常使用我们还是习惯将这里的T
映射值叫做value
。
7.map
的迭代器和[]
功能样例:
其实这里的代码还可以这样写:
这样写的话,对于已经存在的数据的修改大家都没问题,但是一些同学可能会疑惑数据第一次出现的时候是怎么统计的呢?
其实在数据第一次出现的时候,会先将这个数据插入,这时候第二个参数就是一个默认值0,之后在进行修改。
operator[]
底层解释:
要搞清楚operator[]
的底层就需要先从insert入手。
可以看到insert
的第一个实现形式的返回值是pair类型,由迭代器和bool值组成。
我们再来看第一种形式的返回值的解读:
这里说的是当该数据是第一次插入的话,就会返回指向这个新插入数据的迭代器,否则就会返回map
中指向该数据的迭代器,第二个bool
值,如果数据是第一次插入的新数据就会返回true
,否则就是返回false
。
因此operator[]
其实大概就是这样实现的:
V& operator[](const K& key)
{
pair<iteraotr,bool> ret = insert(key);
return ret.first->second;
}
8. multimap
和map
的差异
multimap
和map
的使用基本完全类似,主要区别点在于multimap
支持关键值key
冗余,那么insert
/find
/count
/erase
都围绕着支持关键值key
冗余有所差异,这里跟set
和multiset
完全⼀样,比如find
时,有多个key
,返回中序第一个。其次就是multimap
不支持[]
,因为支持key
冗余,[]
就只能支持插入了,不能支持修改。
9. 牛刀小试:
(1)题目描述:
(2)思路分析:
首先从题目就能知道这是一个经典的TopK问题,但是与之前不同的是这个是双元素的,因此我们就得用pair
来存储,所以我们的思路就是先用map
来统计各单词的出现次数,再创建小根堆来维护前k个出现次数最多的单词,之后用vector
来存储结果,但由于我们创建的是小根堆,因此在返回结果的时候还需要进行逆置。
(3)代码实现:
(4)题目传送门:
692. 前K个高频单词