【C++进阶】set和map的基本使用

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


一、关联式容器

来自【STL源码剖析】

我们已经接触过STL中的部分容器,例如:vectorlistdeque等,这些容器统称为序列式容器(其底层为线性序列的数据结构)

那关联式容器与序列式容器有什么区别?

所谓关联式容器,就是每个元素都有一个键值(key)和一个实值(value)。当元素被插入到关联式容器中时,容器内部结构可能是红黑树,也可能是哈希表,然后按照其键值key大小,以某种特点规则将这个元素放在一个合适的位置

注意:关联式容器没有所谓头尾(只有最大元素和最小元素),所以不会有所谓push_backpush_frontpop_backpop_front这样的操作行为

一般而言,关联式容器的内部结构是一个平衡二叉树,以便获得良好的搜索效率

二、键值对

在以上我们提到了键值,那么键值是什么呢?

  • 键值对是 一种用来表示具有一一对应关系的结构,该结构中一般只包含两个成员变量:keyvalue。其中,key表示键值,value表示与key对应的信息

关联式容器的实现离不开键值对,因此在标准库中,专门提供了这种pair容器。

pair就不在这里细说了,可以参考往期博客:点击跳转

三、set容器

3.1 概念

【文档介绍】

在这里插入图片描述

  • set其实就是之前在二叉搜索树说的key的模型。但是注意,set并不是完全是二叉搜索树,它的底层其实是一颗红黑树(一颗搜索树)

  • set不允许有两个元素有相同的键值key

3.2 构造函数

在这里插入图片描述

  1. 默认构造
  2. 迭代区间构造
  3. 拷贝构造

和以往学习的STL容器类似,就不一一演示了

3.3 迭代器

在这里插入图片描述

为什么迭代器遍历出来的结果为升序呢?我们可以联想【二叉搜索树】,它的中序遍历是一个升序。因此,set容器底层迭代器的实现方式就是中序。

那么接下来又牵扯到一个知识点:如果一个容器支持迭代器,那么它必定支持范围for,因为其底层就是靠迭代器实现的

在这里插入图片描述

3.4 insert

【函数原型】

在这里插入图片描述

那如果我在set容器中插入重复元素,结果又会是如何呢?

在这里插入图片描述

我们发现:当插入的数出现冗余,它就有去重的效果。其实它的底层其实是:遇到键值冗余的数据就不插入。

3.5 find

【函数原型】

在这里插入图片描述

【返回值】

在这里插入图片描述

【代码样例】

在这里插入图片描述

  • 问题1:似乎算法库algorithm中也有一个find函数,那么为什么set容器还要再设计find函数呢?

它们除了功能是相同的以为,在 效率方面不是一样,set实现的find函数查找的时间复杂度是O(logN),大的往右子树查找,小的往左子树查找,因此只需要查找高度次;而算法库中的find是利用迭代器来遍历整颗树(暴力查找),时间复杂度为O(N)因此,实际中有运用到find函数,建议使用内置的。

  • 问题2:我们可以通过迭代器改变set的元素值吗?

在这里插入图片描述

答案已经很明显了!不能修改!因为set元素值就是其键值,修改了会关系到set元素的排列规则。如果任意改变set元素值,会严重破坏set组织。通过查阅文档,我们发现普通迭代器被定义为const迭代器

在这里插入图片描述

3.6 erase

【函数原型】

在这里插入图片描述

大家看上图,迭代器方式删除和给值删除方式有没有什么差异? 给值删除不是更加方便吗?这两个删除有什么差异呢?

大家可以认为给值删除就是依靠第一种实现的。这是因为在大多情况,还是要依靠查找来删除值。

除此之外,给值删除方式有个特点:删除除容器以外的值不会报错

在这里插入图片描述

3.6 count

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

  • 除了find可以查找,count也可以。count作用是:传一个值,就会返回这个值出现了几次。因此,它也可以用来查找。

  • 如果值存在,那么它百分百会返回1,因为set不允许出现键值冗余的情况。

在这里插入图片描述

但是有了find函数,再设计count函数是不是有点冗余。其实count在后面另有用处…

四、multiset容器

在这里插入图片描述

set 还有一个亲兄弟:multiset,它和set容器最大的区别是:允许出现数据冗余,即插入冗余的数据一定是成功的。

除此之外,multisetset的操作没什么区别,都是一模一样。大家可以通过文档自行查阅:点击跳转

这里单独演示一下允许数据冗余的效果:

在这里插入图片描述

所以,multiset 才是真正的排序,可以出现数据冗余的情况,set则是去重 + 排序

除此之外,刚刚说的 count函数其实就是为multiset准备的

在这里插入图片描述

在这里插入图片描述

那么问题来了,如果我想查找的数据恰好出现冗余的情况,请问返回的数据是第几次出现的数据呢?

可以打印出它们的地址来看

#include <iostream>
#include <vector>
#include <set>
using namespace std;

int main()
{
	vector<int> v = { 1,3,5,7,5,9,2,3,3,3 };
	multiset<int> ms1(v.begin(), v.end());

	auto pos = ms1.begin();
	while (pos != ms1.end())
	{
		cout << *pos << ":" << &(*(pos)) << endl;
		pos++;
	}
	cout << endl;

	// 查找5
	cout << "5:" << &(*(ms1.find(5))) << endl;

	return 0;
}

【输出结果】

在这里插入图片描述

在实际中,multiset用的比较少,重点掌握set即可

五、map容器

5.1 概念

在这里插入图片描述

  • map是二叉搜索树改造后的key/value模型,是一个真正意义上的键值对容器。
  • map的特性:所有元素都会根据元素的键值自动被排序map的所有元素都是用pair结构存储的,同时拥有实值(value)和键值(key)。pair的第一个元素(first)被视为键值,第二个元素(second)被视为实值。
  • 注意:map不允许两个元素拥有相同的键值key

5.2 insert

【函数原型】

在这里插入图片描述

我们首先可以看看插入接口,注意看其参数类型:value_type,它是什么类型呢?可以查阅文档:

在这里插入图片描述

value_type是一个pair结构,并且key_type(也就是键值key)是由const修饰的,表明不能被修改!

接下来举一个代码样例:字典

在这里插入图片描述

插入的方式不能向上面这样写,这是很多初学者犯的错误。刚刚说过,键值和实值需要用pair结构来存

在这里插入图片描述

5.3 访问容器数据 - 迭代器

  • map同样支持迭代器

【代码样例】

在这里插入图片描述

很多初学者大概率会写出以上代码,但是编译不通过!从提示可以看出:pair不支持流插入<<

再回过头来分析:数据是存在pair结构里的,那么像访问结构体内的元素,是不是可以用->或者*操作符来访问数据。并且在【map概念部分】说过了:pair的第一个元素(first)被视为键值,第二个元素(second)被视为实值

在这里插入图片描述

【运行结果】

在这里插入图片描述

同理地,既然map也支持迭代器,那么就必定支持范围for

#include <iostream>
#include <map>
#include <string>
#include <utility>

using namespace std;

int main()
{
    map<string, string> dict;
    pair<string, string> p("插入", "insert");
    dict.insert(p);
    dict.insert(pair<string, string>("插入", "insert"));
    dict.insert(make_pair("删除", "erase"));
    dict.insert({"查找", "find"});

    // 访问for
    for (const auto &x : dict)
    {
        cout << x.first << ":" << x.second << endl;
    }
    return 0;
}

【运行结果】

在这里插入图片描述

那么现在就有一个问题:可以通过map的迭代器改变map的元素内容吗?

  • 如果是想修改键值(key)是不行的。还是和set一样的原因:map元素的键值关系到map元素的排列规则。任意改变map元素的key将会严重破坏map组织。
  • 但如果想要修改元素的实值(value),那么是可以的。因为map元素的实值value并不影响map的排列规则

在这里插入图片描述

官方文档同样也给出了答案:键值keyconst修饰,表示不能被修改。

5.4 operator[]

大家可能会感到奇怪,map底层是一个树形结构,按理来说是不支持随机访问的,可是为什么map支持operator[]呢?

map常见的使用场景是:统计出现过的次数

假设我要写一个统计水果出现的次数,大部分人都可以写出以下代码

#include <iostream>
#include <map>
#include <string>
#include <utility>

using namespace std;

int main()
{
    string s[] = {"西瓜", "西瓜", "香蕉", "苹果", "桃子", "香蕉", "香蕉", "香蕉"};
    map<string, int> dict;
    // 将数组内的元素放进map中来统计次数
    for (auto x : s)
    {
        // 如果水果刚刚出现第一次出现,就将记为1
        map<string, int>::iterator pos = dict.find(x);
        if (pos == dict.end())
        {
            dict.insert(make_pair(x, 1));
        }
        // 否则就是出现过两次以上
        else
        {
            pos->second++;
        }
    }

    for (const auto &x : dict)
    {
        cout << x.first << ":" << x.second << endl;
    }
    return 0;
}

【运行结果】

在这里插入图片描述

但是以上代码还可以优化:使用operator[]

#include <iostream>
#include <map>
#include <string>
#include <utility>

using namespace std;

int main()
{
    string s[] = { "西瓜", "西瓜", "香蕉", "苹果", "桃子", "香蕉", "香蕉", "香蕉" };
    map<string, int> dict;
    // 将数组内的元素放进map中来统计次数
    for (auto x : s)
    {
        dict[x]++;
    }

    for (const auto& x : dict)
    {
        cout << x.first << ":" << x.second << endl;
    }
    return 0;
}

【运行结果】

在这里插入图片描述

代码改进后结果也是正确的,并且比第一种简洁多了!

那么,operator[]到底是何方神圣呢?我们可以通过文档来分析一下:

在这里插入图片描述

在这里插入图片描述

注意看它的参数和返回类型,这里的operator[]并不是以往我们所认识的下标访问。它是通过键值key来返回实值value的引用!

那么这里有个问题,它是如何找到实值,然后进行修改的呢?

文档同样给出了答案,operator[]等价于调用以下这么个长东西

在这里插入图片描述

面对这么长的代码一定要耐下心来从内向外剖析

在这里插入图片描述

因此,就要研究insert的返回值

在这里插入图片描述

简单翻译一下:

insert会返回一个pair结构,其中这个pairfirst被设置成了一个迭代器,而迭代器的指向分两种情况:

  1. 键值key已经在树里,那么就返回树里面key所在结点的迭代器
  2. 键值key不在树里,那么就返回新插入key所在结点的迭代器

因此,(this->insert(make_pair(k,mapped_type()))的返回值是pair<iterator, bool>

在这里插入图片描述
接下来通过解引用,然后.first,相当于获取迭代器指向的结点;最后.second就是得到结点的实值value.

因此,[]就可以通过键值key来修改实值value了。

以上的长代码可以分解如下:

V &operator[](const K &key)
{
    pair<iterator, bool> res = insert(make_pair(key, V()));
    return res.first->second;
}

六、multimap

在这里插入图片描述

multimap中允许出现多个重复的键值。因此,operator[]就无法确认调用者的意图,也就是不知道要返回哪个 键值对应的结点。所以multimap中没有提供operator[],当然其他操作都是和map相同

还有的是:查找find时,返回的是中序遍历中第一次出现元素的迭代器;另外计数count返回的则是当前键值的数量

大家掌握map即可,multimap用得很少

七、交集与差集

7.1 如何查找交集

交集,指两个数组中相同的元素所构成的集合

求交集的步骤如下:

  1. 先将两个数组排序 + 去重。可以用set容器

  2. 遍历两个set容器,如果相等就是交集元素,并且同时++;如果不相等,小的++

  3. 其中一方走完,所有交集就查找完了

相关题目:点击跳转

在这里插入图片描述

7.2 如何查找差集

如果集合A和集合 B 中,只有属于集合 A 但不属于集合 B 的元素所构成的集合,就称为集合A和集合 B 的差集。例如,如果集合 A = {1, 2, 3},集合 B = {2, 3, 4},那么差集就是 {1}

求差集的步骤如下:

  1. 先将两个数组排序 + 去重
  2. 遍历两个容器。如果相等,同时++;如果不相等,记录小的后,再++
  3. 其中一方走完,所有差集就查找完了
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值