C++——STL之map和set

news2024/12/28 8:03:57

C++——STL之map和set

  • 🏐序列式容器和关联式容器
    • 🏀什么是键值对
  • 🏐什么是set
    • 🏀erase
    • 🏀count
    • 🏀lower_bound&&upper_bound
    • 🏀equal_range
  • 🏐multipleset
  • 🏐什么是map
  • 🏐multimap
  • 🏐相关问题
    • 🏀topk问题(前k个高频单词)
    • 🏀两个数组的交集
  • 💬总结

👀先看这里👈
😀作者:江不平
📖博客:江不平的博客
📕学如逆水行舟,不进则退
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
❀本人水平有限,如果发现有错误的地方希望可以告诉我,共同进步👍

🏐序列式容器和关联式容器

C++ 容器大致分为 2 类,序列式容器和关联式容器。

  1. 序列式容器,其存储的都是 C++ 基本数据类型(诸如 int、double、float、string 等)或使用结构体自定义类型的元素。数据之间没有很强的关联性,就是挨着排。
  2. 关联式容器存储的元素,都是一个一个的“键值对”( <key,value> )。如果已知目标元素的键的值,则直接通过该键就可以找到目标元素,而无需再通过遍历整个容器的方式,具有很强的关联性。也是因为这个特性,数据的增删查改相比于序列式容器来说效率更高。

🏀什么是键值对

是一种具有一一对应关系的结构,这种结构有两个成员变量,一个是键值key,另一个是与key对应的value

在SGI-STL中关于键值对的定义:

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

C++ STL 标准库提供了 4 种关联式容器,分别为 map、set、multimap、multiset。他们的底层结构都是红黑树,下面来看一下这些关联式容器吧。

🏐什么是set

以前学的一般都是两个参数,set是三个
在这里插入图片描述
可以看到这里给了一个Compare,和以前的学习优先级队列时的仿函数差不多,可以自己去写定规则去比较,比如T是自定义类型的时候,或者类如Date*这样的,自己需要去写一个比较规则,一个仿函数。
来看一下它的构造:
在这里插入图片描述
有一个全缺省构造,可以看到在构造这里也传这个仿函数进去,还有一个迭代区间构造,一个拷贝构造
这里拷贝构造是深拷贝,要拷贝一棵树,代价极大,所以如果不少特别需要,要谨慎拷贝

key模型支持增删查不支持改,因为本质是一棵搜索树,修改了就乱了。

void test_set()
{
	/*set<int> s;
	s.insert(4);
	s.insert(2);
	s.insert(1);*/
	//我们还可以迭代器区间初始化,如果底层是连续的物理空间,那么原生指针就可以当作天然的迭代器

    //当然还可以像下面这么写,list,vector都可以这么写,这是C++11里增加的列表初始化
	/*set<int> s = { 3, 2, 1, 6, 3, 7, 5 };*/
	
	我们可以用迭代器区间初始化如下面:
	int a[] = { 3, 2, 1, 6, 3, 7, 5 };
	set<int> s(a, a + sizeof(a) / sizeof(int));

这里set直接完成了排序和去重的功能,去重是间接作用出来的,因为这个地方3在第二次插入的时候发现前面插入过了,所以就没有继续插入。

// 排序 + 去重
	set<int>::iterator it = s.begin();
	while (it != s.end())
	{
		//*it = 10;
		cout << *it << " ";
		++it;
	}
	cout << endl;

	for (auto e : s)//可以使用迭代器就可以使用范围for
	{
		cout << e << " ";
	}
	cout << endl;

如果说我们想实现降序的排序,我们怎么可以实现呢?第一种我们可以用反向迭代器,第二种我们可以直接去改变树,我们在初始化插入的时候,迭代器区间去初始化的时候我们这么写(用greater,但是在前面我们要加上#include< function > ):

	set<int, greater<int>> s(a, a + sizeof(a) / sizeof(int));

我们可以看到迭代器在此处的作用,这是一种设计模式,它访问list,map,set,vector容器,没有暴露容器的底层结构,迭代器的优点在于不需要关心底层实现,这是一种封装的体现,你不需要去关心它是链表,顺序表还是说是树,用一种方式做到全都访问。

构造主要支持的核心方式是
那我们现在遍历它主要支持的核心方式是什么呢

🏀erase

erase往往与find分不开,那么下面这两种删除方式有区别吗?

    s.erase(3);
	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的话在这个序列s上是没区别的。但是如果是下面这样就得出的效果就不一样了

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

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

第一个呢,仍然不会报错,但是第二个用迭代器的会报错,这是什么原因呢,原来find在没有找到目标值时会返回end,所以删除出了问题。那么我们可以改进一下代码,在查找后加入判断,如果没有找到目标则不进行删除,找到了再进行删除。如下:

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

	set<int>::iterator pos = s.find(30);
	if(pos!=s.end())
	{
	    s.erase(pos);
	}
	for (auto e : s)
	{
		cout << e << " ";
	}
	cout << endl;

传值传位置都可以,有什么区别呢?感觉传位置还麻烦一些啊,不如传值
确认值在不在就可以用第二种,不然就用删除值的,根据你的需求去选择合适的

🏀count

在这里插入图片描述

考虑这个数在不在的时候可以用count,不过不是为了这里设计的,为的是multiset,允许key值冗余,在这设计的原因是为了保持兼容一致。

🏀lower_bound&&upper_bound

lower_bound低边界的意思,要的是≥这个边界的值,upper_bound是高边界的意思,要的是<这个边界的值,可以说是一个左闭右开区间。测试代码如下:

void test_set2()
{
	std::set<int> myset;
	std::set<int>::iterator itlow, itup;

	for (int i = 1; i < 10; i++) 
	myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90

	itlow = myset.lower_bound(25); // >= val
	itup = myset.upper_bound(60);  // > val             //
	cout << "["<<*itlow <<","<<*itup<<"]"<<endl;

	myset.erase(itlow, itup);                     // 10 20 70 80 90

	std::cout << "myset contains:";
	for (std::set<int>::iterator it = myset.begin(); it != myset.end(); ++it)
	std::cout << ' ' << *it;
	std::cout << '\n';
	}

效果如下:
在这里插入图片描述

🏀equal_range

这里用到了pair键值对,详见下文里map部分内容。
如果所给数是35的话,则得到的是[40,40],如果是40的话,得到的是[40,50],由此可见区间左值要≥所给数,区间右值要>所给数。

void test_set3()
{
	std::set<int> myset;

	for (int i = 1; i <= 5; i++) myset.insert(i * 10);   // myset: 10 20 30 40 50

	std::pair<std::set<int>::const_iterator, std::set<int>::const_iterator> ret;
	ret = myset.equal_range(40); // xx <= val < yy

	std::cout << "the lower bound points to: " << *ret.first << '\n';
	std::cout << "the upper bound points to: " << *ret.second << '\n';
}

🏐multipleset

真正的唯一区别就只有一个,允许键值冗余,用法上完全没有区别,会用set就会用multipleset

存储结构怎么存呢?
这里就不是遍历加去重了,单纯的排序

同时count和find也有点区别
例如find这里当有多个重复值时返回的是中序的第一个,count也不再只有0和1了,会显示个数,

🏐什么是map

在这里插入图片描述
底层存储的结构已经是pair了,pair是一个模板的键值对
pair的定义
在这里插入图片描述
第一个成员叫first第二个叫second
这两个的value type 的typedef是不一样的,pair的key对应的是const的key
在这里插入图片描述

make_pair就是构造一个匿名的pair返回,这样的话就可以去推导不用我们去写类型了,能方便一些。
拷贝和析构与set一样,底层是个树,代价还是挺大的
只要key有了就不允许插入,看key不看val,
make_pair的优势就是自动推导,好处就是不用显式的去写模板参数,那我们为什么不直接构造还调用这个函数,建立栈帧呢?不用担心,这个函数一般会定义成inline。

map的遍历
我们会发现报错,一个函数不会有两个返回值,当里面的数据是一个结构时
统计次数有这么几种方法
常规方法:
方括号:
这里的方括号与以前有些不同,以前相当于是给数组用的,用下标来随机访问,得益于底层空间是连续的,而这里是树形结构,肯定就不是来访问第几个了,这里是给一个key返回对应的value的引用,方括号一定程度上也代表了插入,如果说没有对应的映射类型,将会调用它的默认构造 (the element is constructed using its default constructor)

  • 1.map中有这个key,返回value的引用(查找,修改)
  • map中没有这个key,插入一个pair(key,value()),返回value的引用。(插入+修改)

跟at不一样,at只是查找加修改,如果没找到,就抛异常,而方括号的修改更加多元化。
以前我们写insert都是插入成功返回true,失败返回false,包括写二叉树搜索树都是,这里呢,是返回了个pair,
方括号的底层实现,相当于调用(*((this->insert(make_pair(k,mapped_type()))).first)).
在这里插入图片描述
上面那一大坨看起来很麻烦,我们先来看一下这里的insert
在这里插入图片描述
这里有个bool,就是说判断是否有这个key的,如果没有就插入

  • 1.key已经在map中,返回pair(key_iterator,false);
  • 2.key不在map中,返回pair(new_key_iterator,true);

了解了这些之后就可以把上面那个一大坨看起来很麻烦的简化一下

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

所以我们更喜欢用方括号,功能多样,增改都有了,比起insert更喜欢用方括号来插入

🏐multimap

与map相比,没有方括号了,是因为允许键值冗余。find查找的是中序的第一个,其他基本差不多。
auto 用的前提是推导,右边的值推到左边,如果不能就别用auto

🏐相关问题

🏀topk问题(前k个高频单词)

前k个高频单词
思考:这个问题可以看到要求双排序,我们可以先统计次数。统计完之后我们要考虑topk,很多人都会想到堆,就是优先级队列,建一个大堆,依次去取,取出来后放进去,或者说小堆,但是小堆主要应对大量数据,这地方没必要,这里用优先级队列的话,有一个问题是不能保证出现次数相同的谁在前谁在后,重要的是字典序排序也很难。用优先级队列可不可以搞,可以,但还要上其他的东西或者说搞仿函数。优先级队列默认传的是小于,碰到次数相等尝试去比较key。

class Solution {
public:
        struct Less
        {
            bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) const
            {
                if(kv1.second<kv2.second)
                {
                    return true;
                }
                if(kv1.second==kv2.second&&kv1.first>kv2.first)
                {
                    return true;
                }
                return false;
            }
        };
    vector<string> topKFrequent(vector<string>& words, int k) {
        //统计次数
        map<string,int> countMap;
        for(auto&str: words)
        {
            countMap[str]++;
        }
        //topk
        // priority_queue<pair<string,int>,vector<pair<string,int>>,Less<pair<string,int>>> maxHeap;
        // for<auto& kv: countMap)
        // {
        //     maxHeap.push(kv);
        // }
        typedef priority_queue<pair<string,int>,vector<pair<string,int>>,Less> MaxHeap;
        MaxHeap mh(countMap.begin(),countMap.end());

        vector<string> v;
        while(k--)
        {
            v.push_back(mh.top().first);
            mh.pop();
        }
        return v;

    }
};

发现通过仿函数的方式用优先级队列也可以完成。
那我们用其他的方法呢?sort是不稳定的,我们改变它的比较方法让其稳定了,和上面那个优先级队列没啥区别都用到了仿函数,只是下面这个地方我们用到了算法

class Solution {
public:
        struct Greater
        {
            bool operator()(const pair<string,int>& kv1,const pair<string,int>& kv2) const
            {
                if(kv1.second>kv2.second)
                {
                    return true;
                }
                if(kv1.second==kv2.second&&kv1.first<kv2.first)
                {
                    return true;
                }
                return false;
            }
        };
    vector<string> topKFrequent(vector<string>& words, int k) {
        map<string,int> countMap;
        for(auto&str: words)
        {
            countMap[str]++;
        }
        for(auto&kv:countMap)
        {
            cout<<kv.first<<":"<<kv.second<<endl;
        }
        vector<pair<string,int>> sortv(countMap.begin(),countMap.end());
        sort(sortv.begin(),sortv.end(),Greater());

        vector<string> v;
        for(size_t i=0;i<k;++i)
        {
            v.push_back(sortv[i].first);
        }
        return v;

    }
};

1.这个地方sort也可以换成stable_sort,这样仿函数就不用多比较一下。2.算法需要随机迭代器所以这个地方用vector转了一下。
那如果不用仿函数可以做吗?我们要进行排序,除了考虑排序算法我们还可以考虑map和set,只不过是对key排序,但是这里不能用map是因为,map是去重+排序,所以要用multimap

class Solution {
public:
    vector<string> topKFrequent(vector<string>& words, int k) {
    //统计次数,默认按string去排序
        map<string,int> countMap;
        for(auto&str: words)
        {
            countMap[str]++;
        }
        multimap<int,string,greater<int>> sortMap;
        for(auto&kv:countMap)
        {
            sortMap.insert(make_pair(kv.second,kv.first));
        }

        vector<string> v;
        multimap<int,string,greater<int>>::iterator it=sortMap.begin();
        for(size_t i=0;i<k;++i)
        {
            v.push_back(it->second);
            ++it;
        }
        return v;

    }
};

注意这里它是中序来比较,有的人觉得我这里排降序,然后用反向迭代器也可以,但是在multimap时已经按字典序排过了,倒着取就不对了。
这个地方同stablesort是一样的,保证稳定是保证次数相同的稳定。

🏀两个数组的交集

两个数组的交集
判断在不在另一个数组里呢?肯定是用set更好,因为它快啊,比我们直接去用find快,算法里也有find但是是暴力查找,复杂度为O(N),另一个是O(lgN)。
放进set去重,记住!!搞交集一定要去重,因为如下两个数组nums1 = [1,2,2,1], nums2 = [2,2],当2去第一个数组里查时发现重复所以放到vector里,那当第二个2去查时也会发现重复又放到vector里,所以一定要去重!!
当然算法里也有个东西可以做到去重,叫unique,这是去重算法,你可以先sort一下,再unique。这个是没有删除元素的,是通过覆盖的方式,最后resize一下实现的,这也在刚学习数据结构时顺序表部分有类似操作。
这里我们搞个新思路,不用在不在的思想,假设已经去过重了,交集差集怎么得到呢?这里有个算法思想可以很好的解决这个问题。
两个下标位置依次走

找交集
1.小的++
2.相等的就是交集,同时++
3.其中一个结束了,交集就找完了
找差集
1.小的就是差集
2.相等就同时++
3.其中一个结束了,另一个剩下的部分就是差集

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 it1=s1.begin();
        auto it2=s2.begin();
        vector<int> v;
        while(it1!=s1.end()&&it2!=s2.end())
        {
            if(*it1<*it2)
            {
                ++it1;
            }
            else if(*it2<*it1)
            {
                ++it2;
            }
            else
            {
                v.push_back(*it1);
                ++it1;
                ++it2;
            }
        }
        return v;

    }
};

💬总结

  • 找交集时一定要记得去重!!

在这里插入图片描述

觉得还不错的铁汁点赞关注一下吧😀

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1465667.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【办公类-16-10-01】“2023下学期 中4班 自主游戏观察记录(python 排班表系列)

背景需求 上学期的周安排里&#xff0c;每班每周的自主游戏会轮到多个不同的内容 因此在每周的自主游戏观察有2次记录&#xff0c;观察的项目可以写不一样的&#xff0c; 如一位老师写沙水游戏&#xff0c;另一位写表演游戏 本学期&#xff0c;中班的自主游戏全部是户外的&am…

[算法沉淀记录] 排序算法 —— 归并排序

排序算法 —— 归并排序 算法介绍 归并排序是一种分治算法&#xff0c;由约翰冯诺伊曼在1945年发明。它的工作原理是将未排序的列表划分为n个子列表&#xff0c;每个子列表包含一个元素(包含一个元素的列表被认为是有序的)&#xff0c;然后重复合并子列表以生成新的有序子列表…

​LeetCode解法汇总2583. 二叉树中的第 K 大层和

目录链接&#xff1a; 力扣编程题-解法汇总_分享记录-CSDN博客 GitHub同步刷题项目&#xff1a; https://github.com/September26/java-algorithms 原题链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给你一棵二叉树的根节点 root 和一个正整…

Redis进阶篇

Redis线程模型 redis是基于内存运行的高性能k-v数据库&#xff0c;6.x之前是单线程, 对外提供的键值存储服务的主要流程 是单线程&#xff0c;也就是网络 IO 和数据读写是由单个线程来完成&#xff0c;6.x之后引入多线程而键值对读写命 令仍然是单线程处理的&#xff0c;所以 …

有趣且重要的JS知识合集(19)前端实现图片的本地上传/截取/导出

input[file]太丑了&#xff0c;又不想去改button样式&#xff0c;那就自己实现一个上传按钮的div&#xff0c;然后点击此按钮时&#xff0c;去触发file上传的事件, 以下就是 原生js实现图片前端上传 并且按照最佳宽高比例展示图片&#xff0c;然后可以自定义截取图片&#xff0…

Cesium入门基础一:cesium加载地球与环境搭建

一、cesium加载地球与环境搭建 1、搭建vue3项目 使用vite进行搭建。 npm init vitelatest根据操作提示选择&#xff1a;vue 3 Typescript 2、下载cesium库 终端输入&#xff1a; npm install cesium3、创建cesium地图容器 <template><div id"view_contai…

C 标准库 - <errno.h>

在C语言编程中&#xff0c;<errno.h> 头文件扮演着至关重要的角色&#xff0c;它提供了一个全局变量 errno 以及一系列预定义宏&#xff0c;用于指示系统调用或库函数执行过程中发生的错误。这些宏有助于程序员诊断和处理运行时错误。 errno 变量 extern int errno;err…

【文件搜索项目】使用jdbc操作SQLite

一. 插入&#xff08;采用变量的方式插入&#xff09; 1.创建数据源.DateSource 2.建立连接 3.构造SQL语句 4.执行SQL语句 5.释放资源 public class TestSQLite {public static void main(String[] args) throws SQLException {textInsert();}public static void textInsert(…

IO进程线程的通信操作

1.编程实现互斥机制 程序代码&#xff1a; 1 #include<myhead.h>2 int num520;//临界资源3 //1.创建一个互斥锁变量4 pthread_mutex_t mutex;//定义一个pthread_mutex_t类型的变量5 //定义任务1函数6 void *task1(void *arg)7 {8 printf("不畏过去\n");9 …

【前端素材】推荐优质后台管理系统GramOs平台模板(附源码)

一、需求分析 后台管理系统是一种用于管理网站、应用程序或系统的工具&#xff0c;它通常作为一个独立的后台界面存在&#xff0c;供管理员或特定用户使用。下面详细分析后台管理系统的定义和功能&#xff1a; 1. 定义 后台管理系统是一个用于管理和控制网站、应用程序或系统…

git切换仓库地址

已有git仓库&#xff0c;要切换提交的仓库地址&#xff0c;用以下命令 git remote set-url origin 自己的仓库地址 用以下命令&#xff0c;查看当前仓库地址&#xff1a; git remote show origin 切换仓库后&#xff0c;用以下命令初始化提交仓库&#xff1a; git push -u o…

cocos creator3.x项目打包成aar 加入到已有的Android工程

Cocos crearor版本&#xff1a; 3.4.2 Android Studio Flamingo | 2022.2.1 Patch 2 1、配置构建安卓项目 2、 运行编译无报错 出现问题可尝试修改Gradle版本 修改jdk版本 3、对libservice打包成aar 打包完后 再build/outputs找到aar 如果看不到Tasks模块&#xff0c;在Fil…

连接查询(学习笔记)

通过对DQL的学习&#xff0c;我们可以很轻松的从一张数据表中查询出需要的数据&#xff1b;在企业的应用开发中&#xff0c;我们经常需要从多张表中查询数据&#xff08;例如&#xff1a;我们查询学生信息的时候需要同时查询学生的班级信息&#xff09;&#xff0c;可以通过连接…

【Spring】IoC容器 控制反转 与 DI依赖注入 概念 第一期

文章目录 Spring 和 SpringFramework概念一、Spring IoC容器 核心概念1.1 组件和组件管理概念1.2 Spring IoC容器和容器实现1.2.1 普通和复杂容器1.2.2 SpringIoC容器介绍1.2.3 SpringIoC容器具体接口和实现类1.2.4 SpringIoC容器管理配置方式 1.3 Spring IoC / DI概念总结二、…

Electron实战之环境搭建

工欲善其事必先利其器&#xff0c;在进行实战开发的时候&#xff0c;我们最终的步骤是搞好一个舒服的开发环境&#xff0c;目前支持 Vue 的 Electron 工程化工具主要有 electron-vue、Vue CLI Plugin Electron Builder、electron-vite。 接下来我们将分别介绍基于 Vue CLI Plu…

DataDreamer:让创建自定义数据集轻松无比!还自带标注!

编辑&#xff1a;OAK中国 首发&#xff1a;oakchina.cn 喜欢的话&#xff0c;请多多&#x1f44d;⭐️✍ 内容可能会不定期更新&#xff0c;官网内容都是最新的&#xff0c;请查看首发地址链接。 ▌前言 Hello&#xff0c;大家好&#xff0c;这里是OAK中国&#xff0c;我是Ash…

数字信号处理:傅里叶分析

本文主要参考视频如下&#xff1a; 数字信号处理9-1_线性时不变系统对复指数信号的响应_哔哩哔哩_bilibili 傅里叶分析的主要研究内容如下所示&#xff1a; 注意&#xff0c;计算机中使用的离散傅里叶变换并不是离散时间傅里叶变换&#xff1b; 前四种都是理论上的变换方式&…

Graphpad Prism10.2.0(329) 安装教程 (含Win/Mac版)

GraphPad Prism GraphPad Prism是一款非常专业强大的科研医学生物数据处理绘图软件&#xff0c;它可以将科学图形、综合曲线拟合&#xff08;非线性回归&#xff09;、可理解的统计数据、数据组织结合在一起&#xff0c;除了最基本的数据统计分析外&#xff0c;还能自动生成统…

springboot大学生体质测试管理系统源码和论文

大学生体质测试管理系统提供给用户一个简单方便体质测试管理信息&#xff0c;通过留言区互动更方便。本系统采用了B/S体系的结构&#xff0c;使用了java技术以及MYSQL作为后台数据库进行开发。系统主要分为系统管理员、教师和用户三个部分&#xff0c;系统管理员主要功能包括首…

vue-router 三级路由,路由跳转页面异常白屏或404,或刷新三级路由页面后一级和二级路由菜单丢失

问题描述 情况1. vue-router 定义三级路由&#xff0c;路由跳转了&#xff0c;页面404或者白屏情况2. 点击菜单三级路由后&#xff0c;刷新页面后一级和二级路由菜单丢失 解决方案&#xff1a; 某些时候是因为二级和三级的路由共用router-view&#xff0c;可以使用router-vi…