当前位置: 首页 > news >正文

C++:关联式容器的介绍及map与set的使用

我们之前已经学习过string,vector,list,queue,priority_queue等容器,这些容器我们统称为序列式容器,因为它们的数据的逻辑结构呈线性。因为这些容器中存储的数据即便二者之间发生交换,也不会对原有的容器结构造成太大影响。

但上篇文章我们介绍到了二叉搜索树,如果之前有在leetcode上刷过题目的朋友肯定会遇到哈希表这一种题型标签。它的最简单的形式就是我们的鸽巢数组,我们能够通过下标来快速索引目标值以O(1)的时间复杂度。这种容器其实就是我们今天所要介绍的关联式容器,因为它们的位置和自身存储的值都有着密切关系,一旦数据发生交换,原有的容器结构将会造成极大的破坏,我们先从最简单的set和multiset关联式容器开始介绍:

一,set系列容器

<set> - C++ Reference

 set的底层是一棵红黑树。但我们这里先暂且不介绍红黑树的模拟实现,我们先来了解set系列容器的用法,这会对我们之后文章模拟实现红黑树有很大帮助:

1.1set类的介绍

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;

可以看到大致上与我们之前所学容器一致:1.键值(k)类型2.升或降序(仿函数,注意这里的反人设计,我们在优先级队列处也知道,传less<T>建的是大堆,而greater<T>则是小堆,这里也类似)。

其他的几点注意事项:

  1. set底层存储数据的内存是从空间配置器申请的,如果需要可以⾃⼰实现内存池,传给第三个参数。
  2. ⼀般情况下,我们都不需要传后两个模版参数
  3. set底层是树实现,增删查效率是O(logN),迭代器遍历是的搜索树的中序,所以是有序的。 

 1.2set的构造和迭代器

set的持正向和反向迭代遍历,遍历默认按升序顺序,因为底层是叉搜索树,迭代器遍历的中序,持迭代器就意味着⽀持范围for,set的iterator和const_iterator都不持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。 

1.3set的增删查改 

// 单个数据插⼊,如果已经存在则插⼊失败
pair<iterator, bool> insert(const value_type& val);
// 列表插⼊,已经在容器中存在的值不会插⼊
void insert(initializer_list<value_type> il);
// 迭代器区间插⼊,已经在容器中存在的值不会插⼊
template <class InputIterator>
void insert(InputIterator first, InputIterator last);
// 查找val,返回val所在的迭代器,没有找到返回end()
iterator find(const value_type& val);
// 查找val,返回Val的个数
size_type count(const value_type& val) const;
// 删除⼀个迭代器位置的值
iterator erase(const_iterator position);
// 删除val,val不存在返回0,存在返回1
size_type erase(const value_type& val);
// 删除⼀段迭代器区间的值
iterator erase(const_iterator first, const_iterator last);
// 返回⼤于等val位置的迭代器
iterator lower_bound(const value_type& val) const;
// 返回⼤于val位置的迭代器
iterator upper_bound(const value_type& val) const;

重点了解以上几个即可,我们注意到上面出现了一个我们之前没有见过的新类型pair,这个我们下面介绍map时会着重介绍。

这里读者可以自行下去进行实验,与我们所学过的二叉搜索树大致用法相同。

1.3multiset与set的区别 

set所存储的数据中,不会出现重复的元素。而multiset允许重复元素的存在,这就是这二者的最大区别。需要注意的是,如果我们想要删除某一特定键值结点,我们必须要传入它的迭代器位置,而不是传入它的迭代器的值,后者会把所有与输入键值相同的结点完全删除。

1.4以set的视角来看待两道力扣题 

. - 力扣(LeetCode)

这道题要求我们去找两个数组的交集,如果是我们还没有学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());auto i = s1.begin(),j = s2.begin();vector<int> ans;while(i != s1.end() && j != s2.end()){if(*i < *j) i++;else if(*i > *j) j++;else{ans.push_back(*i);i++;j++;}}return ans;}
};

虽然时间复杂度与前者别无二至,但确实是一种在博主看来比较新颖的一种做法。

 下面这道题我们在学习链表数据结构时做过,就是leetcode的环形链表II,当时我们第一次做的时候可能还废了点力气去推导为甚么快指针最后一定会与慢指针相遇且在环口处相遇。但这里如果用上了set那简直就是降维打击:

. - 力扣(LeetCode)

class Solution {
public:ListNode *detectCycle(ListNode *head) {set<ListNode*> s;ListNode* cur = head;while(cur){auto ret = s.insert(cur);if(ret.second == false)return cur;cur = cur->next;}return nullptr;}
};

简介明了,而且在使用过set后会非常容易想到,虽然要耗费额外的空间,但在大家都会双指针的情况下,这种方式是不是更新颖,而且能够耗费更少的时间去推导呢?所以不仅仅是对博主自己的建议,也是对大家的建议,或许我们选择解法时都会尽可能的选择最优算法,但最优的算法往往没有那么容易想到,或许我们退而求其次,不追求极致的优化,或许也会逊色于最优,但却也能让人眼前一亮。

二,Pair类型介绍

map底层的红树节点中的数据,使pair存储键值对数据。 所以在介绍map之前,我们先来了解一下pair。有助于我们接下来了解 map。

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));
}

 现在我们转过来了解下set中的这串声明:

// 单个数据插⼊,如果已经存在则插⼊失败
pair<iterator, bool> insert(const value_type& val);

我们知道,在set中插入元素后,根据我们以往的经验,它应该是返回插入元素的下标。但这里又多了个bool。因此我们结合上面pair的定义可以大致推出,无论是否插入成功,insert函数都会返回插入元素的迭代器所在位置,而bool类型数据则是记录插入是否成功:

//比如我们有以下一个定义式
pair<iterator,bool> n = Set.insert(9);//n.first = ...:插入的数据的迭代器位置
//n.second = true/false//插入成功/失败

三,map系列容器

 3.1map的定义

<map> - C++ Reference

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;

 3.2map的构造

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();

3.3map的增删查

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;

3.4map的数据修改 

map第⼀个持修改的式时通过迭代器,迭代器遍历时或者find返回key所在的iterator修改,map 还有⼀个常重要的修改接operator[],但是operator[]不仅仅持修改,还持插数据和查找数据,所以他是⼀个多功能复合接需要注意从内部实现度,map这把我们传统说的value值,给的是T类型,typedef为 mapped_type。value_type是红树结点中存储的pair键值对值。常使我们还是习惯将这的T映射值叫做value。

value_type->pair<const key_type, mapped_type>
// 查找k,返回k所在的迭代器,没有找到返回end(),如果找到了通过iterator可以修改key对应的
//mapped_type值
iterator find(const key_type& k);
// ⽂档中对insert返回值的说明
// The single element versions (1) return a pair, with its member pair::first
//set to an iterator pointing to either the newly inserted element or to the
//element with an equivalent key in the map.The pair::second element in the pair
//is set to true if a new element was inserted or false if an equivalent key
//already existed.
// insert插⼊⼀个pair<key, T>对象
// 1、如果key已经在map中,插⼊失败,则返回⼀个pair<iterator,bool>对象,返回pair对象
//first是key所在结点的迭代器,second是false
// 2、如果key不在在map中,插⼊成功,则返回⼀个pair<iterator,bool>对象,返回pair对象
//first是新插⼊key所在结点的迭代器,second是true
// 也就是说⽆论插⼊成功还是失败,返回pair<iterator,bool>对象的first都会指向key所在的迭
//代器
// 那么也就意味着insert插⼊失败时充当了查找的功能,正是因为这⼀点,insert可以⽤来实现
//operator[]
// 需要注意的是这⾥有两个pair,不要混淆了,⼀个是map底层红⿊树节点中存的pair<key, T>,另
//⼀个是insert返回值pair<iterator, bool>
pair<iterator, bool> insert(const value_type & val);
mapped_type& operator[] (const key_type& k);
// operator的内部实现
mapped_type& operator[] (const key_type& k)
{// 1、如果k不在map中,insert会插⼊k和mapped_type默认值,同时[]返回结点中存储// mapped_type值的引⽤,那么我们可以通过引⽤修改返映射值。所以[]具备了插⼊ + 修改功能// 2、如果k在map中,insert会插⼊失败,但是insert返回pair对象的first是指向key结点的//迭代器,返回值同时[]返回结点中存储mapped_type值的引⽤,所以[]具备了查找 + 修改的功能pair<iterator, bool> ret = insert({ k, mapped_type() });iterator it = ret.first;return it->second;
}

 3.5map与multimap的区别

multimap和map的使基本完全类似,主要区别点在于multimap持关键值key冗余,那么 insert/find/count/erase都围绕着⽀持关键值key冗余有所差异,这跟set和multiset完全⼀样,如find时,有多个key,返回中序第⼀个。其次就是multimap不⽀持[],因为⽀持key冗余,[]就只能持插⼊了,不能持修改。 

下面两道是可以使用map来解决的两道题目,这里我们就不再给出解决方法了,我们可以先用一般思路来做这两道题,之后再用map相关知识来做,能够对map有一个更深的了解:

. - 力扣(LeetCode)

. - 力扣(LeetCode)

http://www.lryc.cn/news/486878.html

相关文章:

  • 一文说清:Linux下C++静态库的封装和调用
  • 【Java 学习】数据类型、变量、运算符、条件控制语句
  • 【软考】系统架构设计师-数据库设计基础
  • 【Jmeter相关】
  • 拍立淘按图搜索API接口系列,返回示例图参考
  • OSG开发笔记(三十二):深入理解相机视口、制作支持与主视图同步变换旋转的相机HUD
  • 2024RISC-V中国峰会 演讲幻灯片和视频回放均已公开
  • 河道无人机雷达测流监测系统由哪几部分组成?
  • 28.<Spring博客系统⑤(部署的整个过程(CentOS))>
  • OpenAI震撼发布:桌面版ChatGPT,Windows macOS双平台AI编程体验!
  • 香港站群服务器有助于提升网站在搜索引擎中的排名
  • YOLOX:使用自己数据集训练模型及改进--1.YOLOX环境搭建及运行
  • PyTorch使用教程-深度学习框架
  • TON商城与Telegram App:生态融合与去中心化未来的精彩碰撞
  • “乐鑫组件注册表”简介
  • 凹凸/高度贴图、法线贴图、视差贴图、置换贴图异同
  • ZSTD 内存泄漏问题
  • c# npoi操作excel
  • 十二:HTTP错误响应码:理解与应对
  • Rust学习(六):函数式编程
  • 使用 Vue 和 Create-Vue 构建工程化前端项目
  • opencv图片明暗度判断方法
  • QT6学习第三天
  • 计算机网络-MSTP基础实验一(单域多实例)
  • React合成事件及其核心思想详解
  • Datawhale模型减肥秘籍Tasking之模型量化
  • 在云服务器搭建 Docker
  • Redis 的代理类注入失败,连不上 redis
  • 版本控制【Git Bash】【Gitee】
  • Neo4j Desktop 和 Neo4j Community Edition 区别