C++set和map详细介绍


前言

在本篇文章中,我们将会学到关联式容器set,multiset,map,multimap。
其中前两种容器对应我们上篇二叉树文章中的K模型,后两者容器对应我们的KV模型。
我们来详细看一下吧!!!

一、关联式容器和序列式容器

我们在之前学习到的vector,list,stack,queue等都属于序列式容器,底层是线性结构,只存储数据。
关联式容器也是也是用来存储数据的,不过存储的是<K,V>这样一个键值对。

二者有什么区别呢??
⭐️序列式容器中仅仅存储一个值,关联式容器中存储一个键值对
⭐️序列式容器内部不涉及排序,关联式容器内部自动排序
⭐️序列式容器的查找按照自身的值进行相关查找,关联式容器根据K的值进行查找。
并且关联式速度比序列式容器查找快很多。

键值对

键值对是由<K,V>两个值构成的,其中K叫做键值,V就是与之对应的数据。
比如在日常生活中我们用到的英汉字典,就是一个键值对,每个英文单词对应一个中文翻译。英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。

在C++中STL中已经帮助我们实现了键值对---->(pair)

在这里插入图片描述
T1和T2可以是两个不同的类型,访问是值是first,第二个值是second。

二、set

1.set文档介绍

在这里插入图片描述

看一下模板参数

🌟T:底层存储的数据类型
🌟Compare:比较方式,默认按照小于方式比较
🌟Alloc:set中元素空间的管理方式,使用STL提供的空间配置器管理

set文档介绍

  1. set是按照一定次序存储元素的容器
  2. 在set中,元素的value也标识它(value就是key,类型为T),并且每个value必须是唯一的。set中的元素不能在容器中修改(元素总是const),但是可以从容器中插入或删除它们。
  3. 在内部,set中的元素总是按照其内部比较对象(类型比较)所指示的特定严格弱排序准则进行排序。
  4. set容器通过key访问单个元素的速度通常比unordered_set容器慢,但它们允许根据顺序对子集进行直接迭代。
  5. set在底层是用二叉搜索树(红黑树)实现的。

看一下要点:
⭐️⭐️ set底层是一颗二叉树,内容可以删除,但不允许修改,查找某个元素时间复杂度为logN。
⭐️⭐️ set中不允许出现重复元素,本质是排序+去重
⭐️⭐️ set的正向迭代器遍历是一个升序序列,反向迭代器是一个降序序列。
⭐️⭐️ set存的是一个值value,但是在底层也是一个键值对,不过这个键值对两个值都相同,即<value,value>,我们在使用时,只需要插入就可以,不需要构建键值对。
⭐️⭐️set比较默认按照小于比较
⭐️⭐️对于unique算法,去重相邻重复元素,去重之前需要先排序,才可以达到去重的效果。并不会改变容器的大小,随后调用erase删除这些重复元素。
nums.erase(unique(nums.begin(), nums.end()), nums.end());

2.set成员函数

1.构造函数

在这里插入图片描述
🌟🌟空的set

set< int >s;

🌟🌟迭代器区间构造set

set< int >s(s2.begin(),s2.end());

🌟🌟拷贝构造set

set< int >s(s2);

2.迭代器

在这里插入图片描述

遍历

🌟🌟迭代器遍历

set<int>s;
int a[] = { 8, 3, 1,1,2,3,4,5,6,7,10, 6, 4, 7, 14, 13 };
for (auto& e : a)
{
	s.insert(e);
}
//迭代器遍历
set<int>::iterator it = s.begin();
while (it != s.end())
{
	cout << *it << " ";
	it++;
}
cout << endl;

我们可以看到确实完成了去重任务。
在这里插入图片描述

🌟🌟范围for

	for (auto& e : s)
	{
		cout << e << " ";
	}

3.容量

🌟 🌟 empty
在这里插入图片描述
判断set是否为空

🌟 🌟 size
在这里插入图片描述
找出set中有效元素的个数。

4.修改

🌟 🌟insert
在这里插入图片描述

第一个:直接插入一个值
第二个:在某个位置插入一个值
第三个:插入一个迭代器区间

看起来很简单蛮,我们来看点诡异的,第一个的返回值
pair<iterator,bool> 返回一个键值对??

我们看一下文档的介绍
在这里插入图片描述
如果插入成功就返回插入结点的迭代器,并且返回true。
如果插入失败,也就是set中有这个元素,返回set中这个元素的迭代器,返回false。

🌟 🌟erase
在这里插入图片描述

第一个删除某个迭代器位置的值
第二个删除这个值,注意这个返回值,返回set中val这个元素的个数。
第三个删除一段迭代器区间

第一个删除和第二个删除有什么不同呢??

看一下这段代码,运行结果是什么??

	set<int>s;//空的set
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	for (auto& e : a)
	{
		s.insert(e);
	}
	for (auto& e : s)
	{
		cout << e << " ";
	}
	cout << endl;
	//查找删除
	set<int>::iterator pos = s.find(3);
	s.erase(pos);
	for (auto& e : s)
	{
		cout << e << " ";
	}
	cout << endl;

在这里插入图片描述
成功的把3删除了,那我们如果删除一个不存在的值呢???

	pos = s.find(30);
	s.erase(pos);
	for (auto& e : s)
	{
		cout << e << " ";
	}
	cout << endl;

在这里插入图片描述
系统直接崩溃了,所以我们需要加判断!!!find如果查找不到,返回end这个迭代器
在这里插入图片描述
那如果用第二种方法直接删除一个不存在的值呢??

s.erase(20);

在这里插入图片描述
我们发现并没有报错。

对于直接删除:在就删除,不在不做任何处理

我们要注意这两个的区别!!!!

erase删除一个迭代器区间,删除点的是【first,last)区间中元素(左闭右开)

🌟 🌟swap
在这里插入图片描述
交换两个set的值
🌟 🌟clear
在这里插入图片描述
清空set中的元素

5.其他

🌟 🌟find
在这里插入图片描述
查找某个值,如果存在返回对应的迭代器区间,不存在返回end的迭代器。
🌟 🌟count在这里插入图片描述
记录val这个值的个数,在set中出现了多少次。返回set中值为x的元素的个数

我们也可以用count判断这个元素是否存在。

🌟 🌟lower_bound
在这里插入图片描述
记录大于等于val的迭代器
如果这个值存在,返回这个值的迭代器。
如果这个值不存在,返回大于这个值的迭代器。

在这里插入图片描述
在set中存在3,解引用就返回3。查找9,9不存在,就返回下一个

🌟 🌟upper_bound
在这里插入图片描述
upper_bound也是同样的道理,但是不同的是,这个返回大于val的迭代器。在这里插入图片描述

我们如果想要查找或者删除左闭区间,右闭区间就可以使用这个。

三.multiset

multiset本质和set没有太大差别

我们先来看一下文档
在这里插入图片描述

  1. multiset是按照特定顺序存储元素的容器,其中元素是可以重复的。
  2. 在multiset中,元素的value也会识别它(因为multiset中本身存储的就是<value, value>组成
    的键值对,因此value本身就是key,key就是value,类型为T). multiset元素的值不能在容器
    中进行修改(因为元素总是const的),但可以从容器中插入或删除。
  3. 在内部,multiset中的元素总是按照其内部比较规则(类型比较)所指示的特定严格弱排序准则
    进行排序。
  4. multiset容器通过key访问单个元素的速度通常比unordered_multiset容器慢,但当使用迭
    代器遍历时会得到一个有序序列。
  5. multiset底层结构为二叉搜索树(红黑树)

与set相比,不同的是,multiset允许重复的值存在,本质就是排序。其他的都与set相同。

	set<int>s;
	int a[] = { 8, 3, 1,1,2,3,4,5,6,7,10, 6, 4, 7, 14, 13 };
	for (auto& e : a)
	{
		s.insert(e);
	}
	set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		cout << *it << " ";
		it++;
	}
	cout << endl;
	for (auto& e : s)
	{
		cout << e << " ";
	}

看一下运行结果
在这里插入图片描述
我们再来看一下一个的返回值
在这里插入图片描述
这个地方就有很大的意义了。
在这里插入图片描述
count的意义也就发挥出来了。

四.map

map就是对应我们二叉树中的KV模型

1.map文档介绍

在这里插入图片描述
看一下模板参数
在这里插入图片描述
包括四个值:
🌟Key:键值对中的key值
🌟T:键值对中的value值
🌟Compare:比较器类型,一般是按照小于比较。如果是自定义类型,需要自己传递这个比较方式,达到要求。
🌟Alloc:通过空间配置器来申请底层空间,不需要用户传递,除非用户不想使用标准库提供的
空间配置器

map文档介绍

  1. map是关联容器,它按照特定的次序(按照key来比较)存储由键值key和值value组合而成的元素。
  2. 在map中,键值key通常用于排序和惟一地标识元素,而值value中存储与此键值key关联的内容。键值key和值value的类型可能不同,并且在map的内部,key与value通过成员类型value_type绑定在一起,为其取别名称为pair:
    typedef pair<const key, T> value_type;
  3. 在内部,map中的元素总是按照键值key进行比较排序的。
  4. map中通过键值访问单个元素的速度通常比unordered_map容器慢,但map允许根据顺序对元素进行直接迭代(即对map中的元素进行迭代时,可以得到一个有序的序列)。
  5. map支持下标访问符,即在[ ]中放入key,就可以找到与key对应的value。
  6. map通常被实现为二叉搜索树(更准确的说:平衡二叉搜索树(红黑树))

看一下要点:
🌟🌟map中存储的是一个键值对,键值对中第一个元素,也就是key,根据key值进行排序,确定唯一元素。
第二个值value,与key的内容关联。
🌟🌟元素不可以修改key的值,但是可以修改value的值。
🌟🌟支持下标访问,把key放在[ ]中,可以找到与之对应的value元素。

2.map成员函数

这其中与很多和set的成员函数相同,在这里我就不再重复阐述了。
接下来主要讲述一些不同点,以及重点内容。
在这里插入图片描述

1.构造

🌟🌟1.有名构造
    pair<string, string>p = { “pear”, “梨” };
    m.insert( p);
🌟🌟2.匿名构造
   m.insert(pair<string, string>(“apple”, “苹果”));
🌟🌟3.make_pair
    m.insert(make_pair(“banana”, “香蕉”));
在这里插入图片描述
c++98新增的,本质就是一个函数模板

🌟🌟4.c++11多参数隐式类型转换
    m.insert({“strawberrier” ,“草莓”});

2.insert插入

我们看一下官方文档
在这里插入图片描述
我们主要看一下第一条:

插入的是一个pair类型,返回值也是一个pair类型

我们来看一下有关返回值的介绍
在这里插入图片描述

如果插入到元素存在,返回值中的iterator就指向这个存在的位置的迭代器,bool值为false
如果插入元素不存在,返回值中的iterator就指向这个新插入的位置的迭代器,bool值为true
这里的元素插入是看key的值,与value无关。

3.count

在这里插入图片描述
count返回key为x的键值在map中的个数,注意map中key是唯一的,因此该函数的返回值要么为0,要么为1,因此也可以用该函数来检测一个key是否在map中。

4.迭代器

我们注意一下map有两个值,我们再遍历的时候,不能只遍历一个,要把两个分开。


	for (auto& kv : m)
	{
		cout << kv.first << ":" << kv.second << endl;
	}
	cout << endl;

first代表pair第一个值,second代表pair第二个值。

5.【】和at

map中支持【】访问元素。

如果我们想要统计水果出现的次数
第一步:先看这个水果在不在!!
第二步:如果不在,就把这一个键值对插入进去。如果在,就让vaule这个值加加。
最后遍历一边元素,就完成了

我们再map中不用折磨麻烦,我们看一下这个巧妙的代码。

// 统计水果出现的次数
string arr[] = { "苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
map<string, int> m;
for (auto& e : arr)
{
	m[e]++;
}

m[e]++;就完成了操作。

我们来看一下怎末实现的??
在这里插入图片描述
【】的返回值是value类型,参数是一个key类型。

在文档中还有这样一段话,我们来剖析一下。
在这里插入图片描述
在这里插入图片描述

这个【】需要调用insert,insert上面我们已经介绍过了,拆分来看一下
🌟insert(make_pair(k,mapped_type()))这个是insert的原型,插入一个键值对,key值必须传入,如果value不传入,采用缺省值。
这个k存在,就返回这个k这个值的迭代器,不存在,就返回新插入这个k的迭代器。
🌟this->insert(make_pair(k,mapped_type()))调用这个返回值pair
🌟(this->insert(make_pair(k,mapped_type()))).first调用这个返回值的pair的第一个元素iterator,迭代器本身就是指针。
🌟(*((this->insert(make_pair(k,mapped_type()))).first)).second
这个迭代器指向的第二个元素,就是value.

通过这种方式我们就拿到了value的值
如果不存在,插入成功,返回新插入元素的迭代器
如果已存在,插入失败,返回该key所在位置的迭代器
operator[]函数最后将insert返回值键值对中的value返回

在元素访问时,有一个与operator[]类似的操作at()(该函数不常用)函数,都是通过key找到与key对应的value然后返回其引用
不同的是:当key不存在时,operator[]用默认value与key构造键值对然后插入,返回该默认value,at()函数直接抛异常。

我们看一下这段代码的结果是什么??

	map<string, string> dict;
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("string", "字符串"));
	dict.insert(make_pair("sort", "xxx"));

在这里插入图片描述

只与key的值有关,与value无关。
key相同,value不同,不会插入也不会更新

这个【】就有很多功能了,也可以插入,也可以查找,也可以修改
在这里插入图片描述

五.multimap

map和multimap没有太大区别
在这里插入图片描述

  1. Multimaps是关联式容器,它按照特定的顺序,存储由key和value映射成的键值对<key,value>,其中多个键值对之间的key是可以重复的。
  2. 在multimap中,通常按照key排序和惟一地标识元素,而映射的value存储与key关联的内容。key和value的类型可能不同,通过multimap内部的成员类型value_type组合在一起,value_type是组合key和value的键值对:
    typedef pair<const Key, T> value_type;
  3. 在内部,multimap中的元素总是通过其内部比较对象,按照指定的特定严格弱排序标准对key进行排序的。
  4. multimap通过key访问单个元素的速度通常unordered_multimap容器慢,但是使用迭代器直接遍历multimap中的元素可以得到关于key有序的序列。
  5. multimap在底层用二叉搜索树(红黑树)来实现

multimap和map的唯一不同就是:map中的key是唯一的,而multimap中key是可以重复的。
multimap中没有重载operator[]操作,存在多个key,不确定返回哪一个。
有多个相同的值,使用迭代器,返回的是中序遍历的第一个节点。

六.应用

1.前K个高频单词

我们首先看一下题目介绍

给定一个单词列表 words 和一个整数 k ,返回前 k 个出现次数最多的单词。
返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。

示例 1:
输入: words = [“i”, “love”, “leetcode”, “i”, “love”, “coding”], k = 2
输出: [“i”, “love”]
解析: “i” 和 “love” 为出现次数最多的两个单词,均为2次。
注意,按字母顺序 “i” 在 “love” 之前。

示例 2:
输入: [“the”, “day”, “is”, “sunny”, “the”, “the”, “the”, “sunny”, “is”, “is”], k = 4
输出: [“the”, “is”, “sunny”, “day”]
解析: “the”, “is”, “sunny” 和 “day” 是出现次数最多的四个单词,
出现次数依次为 4, 3, 2 和 1 次。

题目很简单,我们只需要把单词和对应的次数放在map中,按照规则进行排序,最终将前k个值返回就可以。

  vector<string> topKFrequent(vector<string>& words, int k) 
    {
        //放到map中
        map<string,int>mp;
        for(auto&e:words)
        {
            mp[e]++;
        }
        vector<pair<string,int>>vv(mp.begin(),mp.end());
        //按照规则进行排序
        sort(vv.begin(),vv.end(),[](const pair<string,int>p1,const pair<string,int>p2)
        { return (p1.second>p2.second));
        });
        //返回k个
        vector<string>v;
        auto it=vv.begin();
        while(k--)
        {
            v.push_back(it->first);
            it++;
        }
        return v;
    }

我们运行发现是跑不过去的
在这里插入图片描述

原因是因为sort底层是快排,快排不稳定,会打乱按照string比较的顺序。

解决1:stable_sort ,底层是归并,稳定排序。

在这里插入图片描述

解决2:对lambda下手

在这里插入图片描述

2.两个数组的交集

给定两个数组 nums1 和 nums2 ,返回 它们的交集
输出结果中的每个元素一定是唯一的。我们可以不考虑输出结果的顺序 。

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

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

题目要求我们返回的元素必须是唯一的,为我们就可以用set进行去重操作。进行遍历找到两个set的差集。

class Solution {
public:
    vector<int> intersection(vector<int>& num1, vector<int>& num2) 
    {
        //降重+排序
        set<int>s1(num1.begin(),num1.end());
        set<int>s2(num2.begin(),num2.end());

        vector<int>v;
        for(auto&e:s1)
        {
            if(s2.count(e))
            {
                v.push_back(e);
            }
        }
        return v;

    }
};

想要实现实现差集和交集一起找呢??只遍历一遍。

差集:A-B :属于A但是不属于B的

有两个数组,nums1和nums2已经排序
🌟两个依次比较
🌟小的就是差集
🌟相等的就是交集
🌟小的值++
🌟如果两个值相等,同时++

这也称为数据同步算法

比如我们需要用网盘对手机中的照片进行备份

我们有以下几个需求
🌟手机端有的,网盘没有,需要上传到网盘
🌟手机上没有了,但是网盘上还有,需要在网盘上进行删除
这两个就是找差集
🌟每个照片都有最近修改时间。
我对一张照片进行了P图,那么它的最近修改时间就发生了变化,需要重新上传覆盖(交集)。

我们就可以按照这个逻辑重新写一下代码

class Solution {
public:
    vector<int> intersection(vector<int>& num1, vector<int>& num2) 
    {
        //降重+排序
        set<int>s1(num1.begin(),num1.end());
        set<int>s2(num2.begin(),num2.end());

        auto it1=s1.begin();
        auto it2=s2.begin();
        vector<int>v;
        while(it1!=s1.end()&&it2!=s2.end())
        {
            //小的++
            if(*it1<*it2)
            {
                it1++;
            }
            else if(*it1>*it2)
            {
                it2++;
            }
            //同时++
            else
            {
                v.push_back(*it1);
                it1++;
                it2++;
            }
        }
        return v;
        
    }
};

3.随机链表的复制

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。

例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。

在这里插入图片描述

我们如果想要处理next是简单的,困难的就是random的处理。
通过原节点与拷贝节点建立联系,可以通过原节点找到拷贝节点
看看原节点的random连接到哪里,当前节点的拷贝节点就链接到哪里。

class Solution {
public:
    Node* copyRandomList(Node* head) 
    {
        map<Node*,Node*>mp;
        Node*copyhead=nullptr;
        Node*copytail=nullptr;
        Node*cur=head;
        //创建拷贝节点,进行next连接
        while(cur)
        {
            //第一个
            if(copytail==nullptr)
            {
                copytail=copyhead=new Node(cur->val);
            }
            else
            {
                copytail->next=new Node(cur->val);
                copytail=copytail->next;
            }

            //原节点与拷贝节点建立联系
            //通过原节点可以找到拷贝节点
            mp[cur]=copytail;

            //继续遍历
            cur=cur->next;
        }

        Node*copy=copyhead;
        cur=head;
        //random处理
        while(cur)
        {
            copy->random=mp[cur->random];
            copy=copy->next;
            cur=cur->next;
        }
        return copyhead;      
    }
};

总结

以上就是今天要讲的内容,本文仅仅详细介绍了C++map和set的相关内容。希望对大家的学习有所帮助,仅供参考 如有错误请大佬指点我会尽快去改正 欢迎大家来评论~~ 😘 😘 😘

  • 12
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lim 鹏哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值