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

**解锁 C++ std::map 的力量**

前言

        前几天我们探讨了 C++ 中 set 的使用方法,今天咱们就趁热打铁,继续聊聊标准库中另一个非常重要的关联容器——map

1.map类的介绍

        首先,我们可以先看一下在源码中map的声明是什么:

template < class Key, // map::key_type
class T, // map::mapped_type
class Compare = less<Key>, // map::key_compare
class Alloc = allocator<pair<const Key,T> > //
map::allocator_type
> class map;

        在其中,key就是map底层关键字的类型,T是map中value的类型。这里先暂停源码的解释,可能很多读者不知道关键字key和value代表着什么(我上篇文章其实解释过),它们其实集在一起被称之为键值对,键值对的定义在很多语言都涉及到,就例如Python中的字典就是典型的键值对结构。

        让我们通过一个停车场系统的例子来形象化这个概念。在这个系统中,此时的key代表着在停车场系统中我们需要记录的停放车辆的车牌号,而value代表着进入车库时的时间,我们需要通过车牌号(key)来找到车子入库的时间(value)。从这就可以看出key的作用是为了寻找value,并且我们需要记住,在map中,key的值是固定不变的,而value是可以改变的,因为有时候我们需要进入很多次地下停车库,每一次的时间不一样,但是车牌号必须一样,这就是键值对。

        继续往下分析源码,阅读第三个模版参数,可以看出它是仿函数,而map默认是支持小于比较的仿函数(比较的是Key),如果之后我们想要支持大于比较,那可以我们自己实现大于比较的仿函数,然后显示的写出来,之后的参数就是我们熟悉的空间配置器申请的。在一般的情况下,我们都不需要传递后面两个参数,map底层就是红黑树进行实现的,增删查改的效率和set一样还是是O(logN),迭代器遍历也是和二叉树一样走的是中序遍历,所以当我们进行迭代器遍历key的时候,此时也是升序排序的。

        当然,差点忘了告诉各位map的头文件是什么,如下所示。

#include<map>
//map也是在std命名空间里,如果我们没有直接明确using std,那么我们需要指明命名空间。

2.pair类型的介绍

        在介绍 map 之前,有必要先介绍一下 pair 类型,因为在 map 中,键(key)和值(value)是以 pair 的形式存储的。【pair<Key,T>】这样可以帮助大家更好地理解 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;  //对应着map的keyT2 second;  //对应着map的valuepair(): 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是 C++ 标准库中的一个模板类,它用于将两个数据组合成一个整体。这个组合的两个数据类型可以相同,也可以不同。我们通常使用 pair<type1, type2> 来定义一个 pair 对象,分别对应其中的两个元素。

        举个简单的例子:

#include <iostream>
#include <utility>
​
int main() {std::pair<int, std::string> p(1, "apple");std::cout << p.first << " " << p.second << std::endl;return 0;
}
输出结果:1 apple

        在这个例子中,p.first 表示键,p.second 表示值。正是这种结构,使得 map 可以用来存储一组一组的键值对数据。

        并且,map也是和set一样,它是不支持值冗余的,如果想要key冗余,那么可以用另一个容器——multimap。

        接下来,我们就可以开始正式介绍 map 的使用方法和特点。

3.map相关接口介绍

3.1.map的构造

        构造函数是最基本的函数,所以我们上来就应该关注这个接口的使用方法,关于构造的接口,我们无须全部了解,了解下面几个接口就好了。

1.无参默认构造
explicit map (const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());

        这个函数的声明就是上面,我们可以适当的看一下参数,里面第一个参数是一个仿函数,它默认支持的是模版参数中我们默认支持的小于比较的仿函数,我们平常使用的时候是不需要太在意这个参数的,第二个参数是空间配置器,这个也是不用管的,我们使用系统默认的就好了。

        下面我通过一个例子简单的介绍一下map的使用方法。

std :: map<int,int> s1;  //就是这么容易,还可以通过下面的方式书写
std :: map<int,int> s2(); //我一般不习惯这么写
2.迭代器区间构造
template <class InputIterator>
map (InputIterator first, InputIterator last,
const key_compare& comp = key_compare(),
const allocator_type& = allocator_type());

        迭代器区间构造也是我们常常使用到的一个构造方法,里面的参数设置也是十分容易的,第一个参数是开始的迭代器,最后一个参数是最后一个迭代器,不过迭代器区间构造默认也是左闭右开的,所以假设我们想要构造一个map对象的前三个,第一个迭代器直接填begin即可,第二个我们需要填第四个位置的迭代器,确保第三个迭代器在里面。后面两个参数上面介绍过了,这里就不多说了。

        我们依然使用一个例子让各位了解它的用法。

std  :: map<int,int> s1 = {{1,2},{3,4},{5,6}}; //这个用法是通过列表进行构造,后面我会说。
//假设我们需要s1全部的内容(其实用拷贝构造更合理)
std :: map<int,int> s2(s1.begin(),s1.end());
3.initializer list (5) initializer 列表构造
map (initializer_list<value_type> il,
const key_compare& comp = key_compare(),
const allocator_type& alloc = allocator_type());

        这个列表构造是C++11新增的内容,我会在之后出一篇C++11相关的文章来更详细的介绍它,各位目前只要知道用法即可,第一个参数其实就是我们需要填的列表,一般用{}来表示。后面两个参数我在上面讲过,这里就不多说了。

        下面我依然使用一个例子来告知各位了解它的用法。

std :: map<int,int> s1  = {{1,2},{2,4},{23,21}}; 

3.2.map的拷贝构造

map (const map& x);

        拷贝构造函数其实大多数容器用法都一样,它其实就是一次对象的深拷贝,避免浅拷贝造成的多次使用析构函数的问题。

        依然用一个例子展示它的用法。

std :: map<int,int> s1  = {{1,2},{2,4},{23,21}}; 
std :: map<int,int> s2 = s1;  //或者是   std :: map<int,int> s2(s1)  感觉这种方式不好用

3.3.赋值运算符重载

map& operator= (const map& x);   //由于和拷贝构造差不多,我决定不在讲述了

3.4.迭代器

// 迭代器是⼀个双向迭代器
iterator -> a bidirectional iterator to const value_type

        map的迭代器和set一样,都是双向迭代器,关于迭代器的介绍我在上一篇文章讲述过,对此不太了解的读者可以翻看我上一篇的文章。map的迭代器是支持查询key和value值的,因为迭代器的定义中就包含了key和value,这我会在之后的set和map的实现中讲述。

        下面我展示迭代器的几种用法。

std :: map<int,int> s1 = {{1,2},{2,4},{6,7},{3,9}};  //value是可以相同的
//支持迭代器遍历
auto it = s1.begin();
while(it != s1.end())
{std :: cout << it->first << ": " << it->second << " ";it++;
}
std :: cout << std :: endl;
​
//支持迭代器的话,范围for也会支持
for(auto e : s1)
{std :: cout << e.first << ": " << e.second << " ";
}
​
std :: cout << std :: endl;
​
​
//结果:1: 2 2: 4 3: 9 6: 7

3.5.增删查

        讲完了几个最基本的接口,下面我们进行我们的老朋友:增删查的讲解,下面首先进行增相关接口的讲解。

1.增相关接口

1.插入单个值

//1.比较重要的源码
value_type -> pair<const key_type,mapped_type>
// 单个数据插⼊,如果已经key存在则插⼊失败,key存在相等value不相等也会插⼊失败
pair<iterator,bool> insert (const value_type& val);

        增的接口还是比较容易接受的,它和set时的接口是差不多的,此时的返回值也是pair类型的,第一个参数代表着map的迭代器,第二个参数代表着是否插入成功。其实insert的返回值我们多数时间是用不上的,我们仅需插入就好,它是否插入成功都不会影响到后续的结果,因为如果key存在的话,它是不会插入到map中的,不过我们可以在调试的时候使用这个返回值。

        下面我们用个例子简单介绍一下它的使用方法。

std :: map<int,int> s1;
auto it = s1.insert({1,2});  //为了展示它的返回值,这里我直接显示的用返回值了
if(it.second)
std :: cout << it.first->first << ": " << it.first -> second << std :: endl;
auto it1 = s1.insert({1,2});
if(it1.second)
{//........
}
else std :: cout << "数据已经有了" << std :: endl;
​
//结果:
1: 2
数据已经有了

2.列表插入多个值

// 列表插⼊,已经在容器中存在的值不会插⼊
void insert (initializer_list<value_type> il);

        列表插入其实和之前构造函数的列表插入是类似的,里面的参数我们仅需把一个initializer_list列表插入即可,难度不大。

        下面通过一个简单的例子给各位介绍一下它的用法。

std :: map<int,int> s1;
s1.insert({{1,2},{6,7},{3,9},{2,5}});
for(auto e : s1)
{std :: cout << e.first << ": " << e.second() << " ";
}
​
//结果
1: 2 2: 5 3: 9 6: 7

3.迭代器区间插入

// 迭代器区间插⼊,已经在容器中存在的值不会插⼊
template <class InputIterator>
void insert (InputIterator first, InputIterator last);

        其实迭代器插入也是和上面的插入函数是一样的,所以这里我也不详细说了。

        上一个简单的例子吧。

std :: map<int,int> s1;
s1.insert({{1,2},{6,7},{3,9},{2,5}});
std :: map<int,int> s2;
s2.insert(s1.begin(),s1.end());
for(auto e : s2)
{std :: cout << e.first << ": " << e.second() << " ";
}
​
//结果
1: 2 2: 5 3: 9 6: 7
2.找相关接口

1.返回迭代器的版本

// 查找k,返回k所在的迭代器,没有找到返回end()
iterator find (const key_type& k);

        它的用法也是比较容易的,函数的参数我们仅需填入key,就可以通过key来可以找到相关的迭代器,之后通过迭代器就可以找到value,难度不大。

        依旧使用一个比较容易的例子来介绍用法。

std :: map<int,int> s1 = {{1,2}};
auto it = s1.find(1);
if(it != s1.end()) std :: cout << 1 << std :: endl;
else std :: cout << "o(╥﹏╥)o" << std :: endl;
auto it1 = s1.find(2);
if(it1 != s1.end()) std :: cout << 2 << std :: endl;
else std :: cout << "o(╥﹏╥)o" << std :: endl;
​
//结果:
1
o(╥﹏╥)o

2.返回个数的版本

// 查找k,返回k的个数
size_type count (const key_type& k) const;

        这个函数在set也出现过,它的作用就是返回我们所要找寻的key存在的个数。对于map,无非就是两种结果:1或者0,如果是1的话代表存在map中,否则就是没存在(感觉说了一句废话.jpg),它在我们判断key是否在map中有着比较重要的作用,同样的,难度不大。

        来个例子。

std :: map<int,int> s1 = {{1,2}};
if(s1.count(1)) std :: cout << 1 << std :: endl;
else std :: cout << "无" << std :: endl;
if(s1.count(2)) std :: cout << 2 << std :: endl;
else std :: cout << "无" << std :: endl;
​
//结果
1
无
3.删相关接口

1.删除一个迭代器位置的值

// 删除⼀个迭代器位置的值
iterator erase (const_iterator position);

        删除函数本身比较简单,重点在于关注它的返回值。在使用如 erase 这类删除函数时,返回值通常是一个迭代器,指向被删除元素的下一个位置。如果返回的是 end(),说明删除的是容器中的最后一个元素,或者根本没有找到要删除的元素(取决于使用哪种 erase 重载)。因此,若返回的是 end()可能表示删除失败(没有找到元素),也可能仅仅是删除的是最后一个元素,需结合上下文判断。反之则就是证明删除成功,一般的删除函数会搭配着查找函数食用(这也是为什么我们先讲查找函数)。

        同样的,我们通过一个例子进行讲解。

std :: map<int,int> s1 = {{1,2},{2,4}};
auto it = s1.erase(s1.find(1));
if(it != s1.endl()) std :: cout << 1 << std :: endl; 
else std :: cout << 'wu' << std :: endl;
auto a = s1.erase(s1.find(4));
if(a != s1.endl()) std :: cout << 1 << std :: endl;
else std :: cout << "wu" << std :: endl;

2.删除K

// 删除k,k存在返回0,存在返回1
size_type erase (const key_type& k);

        这个删除函数和上面不同的是,它是通过值删除,而上面是通过迭代器进行删除;此时如果删除成功的话,会返回1,删除失败就会返回0。比较容易掌握。

        上一个例子,其实是和上面一样的。

std :: map<int,int> s1 = {{1,2},{2,4}};
if(s1.erase(1)) std :: cout << 1 << std :: endl;
else //.....
if(s1.erase(12)) std ::cout << 2 << std :: endl;
else //......

3.删除一段迭代器区间的值

// 删除⼀段迭代器区间的值
iterator erase (const_iterator first, const_iterator last);

        map中的 erase(first, last) 用于删除一段区间内的键值对,删除的是从 first 到 last(不含)的元素。它的返回值是一个迭代器,指向 last 所在位置,可以用于继续遍历操作。如果 last == map.end(),说明删除到了最后,返回的就是 end()。这个函数常用于批量删除场景。

        下面给出一个例子。

std::map<int, std::string> m = {{1, "one"},{2, "two"},{3, "three"},{4, "four"},{5, "five"}
};
​
auto it1 = m.find(2); // 指向 key=2
auto it2 = m.find(5); // 指向 key=5(不会被删)
​
auto ret = m.erase(it1, it2); // 删除 key=2,3,4
​
std::cout << "返回的迭代器指向的 key 是:" << ret->first << std::endl; // 输出:5

4.map的数据修改

        在前文我说过,map是不支持修改key的值,但是支持修改key对应的value值,如果修改key的话,是会破坏树的结构的。map是可以有两种方式进行数据的修改的(目前我已知)。

1.迭代器修改

        map的第一个支持修改的方式是通过迭代器进行值的修改,当我们在迭代器遍历或者是通过find函数获取一个位置的迭代器时,我们是支持对其中的value进行修改的。

        下面我通过一个例子来让各位知晓它是如何进行修改的。

using namespace std; //不想打std了,怪麻烦的
map<int,int> s1 = {{1,2}};
auto it = s1.find(1);
cout << it -> second << endl;
it -> second = 12;
cout << it -> second << endl;

2.[ ]进行修改

        operator[]是map一个很重要的接口,它是一个复合功能的接口,它不仅支持修改,还支持插入数据和查找数据,可谓是功能贼全,不得不说它支持插入数据,和Python中的[ ]支持插入很想(小编目前在自学Python,所以可能文章涉及的Python知识可能不对,如果有错请私信提醒我),所以可以看出这个接口很是复合。

        为了让各位更好的知晓[ ],下面我给出它的声明,注意:从内部实现角度,map这⾥把我们传统说的value值,给的是T类型,typedef为 mapped_type。而value_type是红黑树结点中存储的pair键值对值。⽇常使⽤我们还是习惯将这里的 T映射值叫做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>
// 查找k,返回k所在的迭代器,没有找到返回end(),如果找到了通过iterator可以修改key对应的
//mapped_type值
iterator find (const key_type& k);
// 需要注意的是这⾥有两个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;
}
​

        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 >。

        所以说,这个[]的功能很全,我们以后在插入单个元素的视时候,完完全全可以用[]来进行使用,查找数据也是类似,通过第二个返回值就可以知道是否可以插入到表中,更不用说数据修改了,和第一个迭代器一比较,这个十分的好用,在某些场景可以说是7优于第一个用法(也不是说第一个用法不好,只能说各有优劣)。

        由于insert比较复杂,所以小编通过一个简单的样例告知各位它的用法。

using namespace std;
map<int,int> s1;
//1.插入数据
s1[1] = 1;
​
//查找数据是否在对象里面。
auto it = s1[3];
if(it.second) cout << "在的兄弟在的" << endl;
else cout << "不在的老兄" << endl;
​
//修改数据
s1[1] = 10086; //10086打钱()

5.map在算法领域的应用

        目前,map在很多算法都有所体现,下面小编留下两个小题,各位读者朋友可以自己做一做,我把思路告知给各位了。

题目1:两数之和

描述: 给定一个整数数组 nums 和一个目标值 target,你需要在数组中找到两个数,它们的和等于目标值,并返回它们的索引。

示例: 输入: nums = [2, 7, 11, 15], target = 9 输出: [0, 1] 解释:nums[0] + nums[1] = 2 + 7 = 9,返回 [0, 1]

思路

  • 使用一个 map 来存储已经遍历过的元素。

  • 在遍历 nums 时,检查当前元素的补数(即 target - nums[i])是否存在于 map 中。如果存在,则找到了两个数的和等于目标值。

  • 如果不存在,将当前元素及其索引存入 map

题目2:两数组的交集

描述: 给定两个数组 nums1nums2,返回它们的交集。输出结果中的每个元素一定是唯一的。

示例: 输入: nums1 = [1, 2, 2, 1], nums2 = [2, 2] 输出: [2]

思路

  • nums1 中的元素存储到 map 中,然后遍历 nums2,检查每个元素是否在 map 中出现过。如果存在,则加入交集数组。

  • 使用 set 来确保交集中的元素唯一。

6.结语

        至此,我们已经全面探讨了 map 容器的使用。在实际开发中,map作为一个高效的数据结构,能够显著提高查找和存储效率,尤其在涉及大量数据处理时尤为重要。通过掌握它的应用,我们能够在多种场景下优化算法,提升程序的性能。

        总的来说,map 的灵活性和高效性使得它成为解决许多算法问题的首选工具。无论是在解决实际问题还是在学习算法的过程中,合理运用 map 都能帮助我们快速而精准地找到解决方案。

        如果文末的算法题有读者朋友不会的话,那么可以私信小编,小编会帮助各位解惑的。

相关文章:

  • 26考研——数据的表示和运算_整数和实数的表示(2)
  • 2025-06-01-Hive 技术及应用介绍
  • 【hive】函数集锦:窗口函数、列转行、日期函数
  • QT的工程文件.pro文件
  • 使用 IntelliJ IDEA 安装通义灵码(TONGYI Lingma)插件,进行后端 Java Spring Boot 项目的用户用例生成及常见问题处理
  • 25.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--用户服务接口
  • SQL手工测试(MySQL数据库)
  • 树莓派超全系列教程文档--(58)通过网络启动树莓派
  • 【LeetCode 热题100】网格路径类 DP 系列题:不同路径 最小路径和(力扣62 / 64 )(Go语言版)
  • 第6章:Neo4j数据导入与导出
  • 自定义连接线程池
  • 408第一季 - 数据结构 - 图
  • mybatis执行insert如何返回id
  • 星耀8上市品鉴暨北京中和吉晟吉利银河用户中心开业媒体见面会
  • 基于多维视角的大模型提升认知医疗过程层次激励编程分析
  • 关于IE浏览器被绑定安装,还卸载不掉
  • RabbitMQ work模型
  • 云原生监控体系建设:Prometheus+Grafana的企业级实践
  • 【11408学习记录】考研写作双核引擎:感谢信+建议信复合结构高分模板(附16年真题精讲)
  • LeetCode - 148. 排序链表
  • 上海市建设监理协会网站查询/温州免费建站模板
  • 网站开发进阶/搜索引擎是什么
  • sharepoint做网站/发布新闻的平台有哪些
  • wordpress搬家 数据库/北京中文seo
  • 贵州网站推广公司/视频网站建设
  • 新手学做网站 pdf 网盘/自己建网站怎么推广