set_map的实现+set/map加持秒杀高频算法题锻炼算法思维

set_map的实现+set/map加持秒杀高频算法题锻炼算法思维

🎬 胖咕噜的稞达鸭个人主页

🔥 个人专栏: 《数据结构《C++初阶高阶》《算法入门》

⛺️技术的杠杆,撬动整个世界!


在这里插入图片描述


在这里插入图片描述


开始进入set和map的学习目录

set类的实现

set的声明:T就是set底层的关键字的类型;
set默认要求支持T比较,如果不支持或者想按照自己的需求走可以自行实现仿函数传给第二个模板参数。
set底层存储数据的内存是从空间配置器申请的,如果需要可以自己实现内存池,传给第三个参数。

set底层是红黑树实现,增删查效率是O(logN),迭代器遍历走的是搜索树的中序,所以是有序的。
中序遍历一定是按照升序排的。

set的构造和迭代器:

来插入结构数字,当set<int>s;就是将set看作是T类型的模板参数。排序加去重的操作。

#include<iostream>#include<set>usingnamespace std;intmain(){ set<int> s;//set<int,greater<int>> s; s.insert(4); s.insert(3); s.insert(8); s.insert(9); s.insert(2); s.insert(6);//set<int>::iterator it=s.begin();auto it = s.begin();while(it != s.end()){ cout <<*it <<" ";//*it = 1;//这里会报错,因为set不支持修改,一旦修改就会破坏结构。++it;} cout << endl;//2 3 4 6 8 9 return0;}

降序:set<int,greater<int>> s;也即是说比根大的数传到左边,比根小的数传到右边。拍出来降序的操作。

还支持插入initializer_list列表值,已经插入的值插入失败。

在这里插入图片描述
s.insert({2,3,4,5});for(auto e : s){ cout << e <<" ";} cout << endl;

set:erase和find

删除最小的值:由于按照升序排序的,所以s.erase(s.begin())会删除最小的值。

s.erase(s.begin());for(auto e : s){ cout << e <<" ";//3 4 6 8 9 } cout << endl;
//直接删除int x; cin >> x;int num = s.erase(x);if(num==0){cout << x <<"不存在"<< endl;}//num==0是指个数,x有几个有一个x,删除成功x就=0了else{ cout << x <<"删除成功"<< endl;}//方法2int x; cin >> x;auto pos = s.find(x);if(pos != s.end()){ s.erase(pos); cout << x <<"删除成功"<< endl;}else{ cout << x <<"不存在"<< endl;}

迭代器失效问题

方法2中pos一定会失效,erase删除了节点,那么指向节点的指针一定会失效,就会出现野指针的问题。
如果删除的是根节点,由于是替代法删除,除了根节点,其他节点都在,虽然没有破坏二叉搜索树的的结构,但是迭代器的意义已经改变了。cout<<*pos<<endl;访问就会报错。

查找:(容器自身的效率更高)

int x;auto pos1 =find(s.begin(), s.end(), x);//算法库中实现的O(N)auto pos2 = s.find(x);//容器自身的O(logN)

set:count

利用count进行间接的查找。如果找到了返回真,不在返回假。

cin >> x;if(s.count(x)){ cout << x <<"在的"<< endl;}else{ cout << x <<"不在"<< endl;}

set:lower_bound和upper_bound

寻找这块区间左闭右开。

#include<iostream>#include<set>usingnamespace std;intmain(){ set<int>mset;for(int i =1; i <10; i++){ mset.insert(i *10);}for(auto e : mset){ cout << e <<" ";}cout << endl;// 10 20 30 40 50 60 70 80 90// 实现查找到的[itlow,itup)包含[30, 50]区间auto itlow = mset.lower_bound(30);//返回>=30auto itup = mset.upper_bound(50);//返回>50 mset.erase(itlow, itup);//删除这段区间的值for(auto e : mset){ cout << e <<" ";}cout << endl;// 10 20 60 70 80 90return0;}

multiset和set

multisetset的使用基本完全类似,主要区别点在于multiset支持值冗余。
也就是multiset仅仅是排序,不去重。

intmain(){ multiset<int>s ={4,2,3,1,5,6,8,6,43,2,5};auto it = s.begin();while(it != s.end()){ cout <<*it <<" ";++it;}cout << endl;int x; cin >> x;auto pos = s.find(x);while(pos != s.end()&&*pos==x){ cout <<*pos <<" ";++pos;}cout << endl;}

问题:那么既然不去重,如果说要找一个在multiset中出现很多次的数字,具体会找哪一个呢?

会找到中序的第一个。这样是有好处的,找到中序的第一个,迭代器不断++会找到multiset的所有x。

map:insert

pair<iterator,bool>insert(const value_type& val)文档中的结构,insert的返回值是pair<iterator,bool>
pair<iterator,bool>insert(const pair<const pair<const key_type,mapped_type>& val>展开来看是这样的。

插入成功:返回pair<新插入值的迭代器,true>
插入失败:返回pair<已经存在的跟key相等的值的迭代器,false>

insert插入一个pair<key,T>对象:

1.如果key已经在map中了,那么就会插入失败,会返回一个pair<iterator,bool>的对象,返回pair对象firstkey所在节点的迭代器,secondfalse;
2.如果key不在map中,插入成功的时候,就会返回一个pair<iterator,bool>的对象返回的pair对象是first新插入key节点所在的迭代器,secondtrue.

所以就是说,insert除了充当插入的 功能,还充当查找的功能。

map:operator[ ]

operator的内部实现:

mapped_type&operator[](const key_type& k){ pair<iterator,bool>ret=insert({k,mapped_type()}); iterator it=ret.first;return it->second;}

这里解释,在map中插入一个k值,插入k值的同时有一mapped_type的键值对。

1.如果k不在map中,在插入k的同时,也将会插入mapped_type的默认构造初始化的参数,不管是什么类型,同时[]会返回节点中存储的mapped_type的值的引用,那么我们就可以通过引用来修改映射值。所以这里[]既有修改又有插入的功能。
2.如果k在map中,insert会插入失败,但是insert返回pair对象的first是指向key节点的迭代器,返回值同时[]返回节点中存储的mapped_type值的引用,所以[]既有查找也有修改的功能。
intmain(){ string myarray[]={"秋","冬","夏","春","夏","秋","夏","春","夏","秋","秋","冬"}; map<string,int>countMap;for(constauto& e : myarray){ countMap[e]++;}//春:2 冬:2 秋:4 夏:4 for(constauto& it : countMap){ cout << it.first <<":"<< it.second << endl;}cout << endl;return0;}

问题:countMap的插入是怎么实现的?

我们知道countMap的功能就是迭代器遍历string类型的数组myarray,
如果元素e本来不在map中,就插入成功,插入e值,同时mapped_type是整型的缺省值,也即是0,bool返回true;
同时返回插入值的迭代器,在pair里面,pair<iterator,bool>,只是返回在pair中,通过pair的迭代器取到first,迭代器用箭头可以取到second,++一下,插入成功,也就是将元素e,缺省值从0+成了1.这样也就是插入成功了。

总结:必看!!!

如果插入成功,说明e不在map中,会插入一个键值对(e,mapped_type的默认构造值),对于什么类型返回对应的默认构造,比如说int默认构造值就是0,double默认构造值就是0.0,同时会返回该键值对的引用。++操作将其从0更新到1;
如果插入失败,说明e在map中,直接返回该键值对对应值的引用。++操作将其值从1更新到2.

multimap和map的差异:

multimapmap的使用基本相似,

1.主要区别在于multimap支持关键值key冗余,也就是说multimap不会去重。那么insert/find/erase都围绕者支持关键值key冗余有所差异,这里find的时候,会有多个key,但是都会返回中序的第一个
2.其次就是multimap不支持[],因为支持key冗余,[]就只能支持插入了,不支持修改

力扣题目链接:

两个数组的交集

两个数组的交集

在这里插入图片描述


题目解析:
题目中有两个数组,要求返回他们的交集,也就是返回的是两个数组中相等的部分。

思路:

相等就是交集,小的++,因为小的一定不是交集。

这里首先用set进行去重+升序,两个数组都需要去重+升序的处理,所以set<int>s1(nums1.begin(),nums1.end()), set<int>s2(nums2.begin(),nums2.end()),

  1. 用两个指针来遍历,分别指向两个数组,
  2. it1指向的值如果小于it2指向的值,就让it1++;直到挪动到和it2指向的相等的值,然后插入到最终返回的vector类型的数组中;
  3. 同理,it2指向的值如果小于it1指向的值,那就让it2++,直到挪动到和it1指向的相等的值,然后插入到最终返回的vector类型的数组中,这里如果两个指针指向是相同的数字,随意返回it1或者it2,只插入一个值。
  4. 如果两个指针都没有指向各自数组的末尾,都需要++;最后走出循环的条件是两个指针有一个走到空就结束循环。
  5. 最后返回ret.
classSolution{public: vector<int>intersection(vector<int>& nums1, vector<int>& nums2){//思路:将两个数组放进set1和set2中,可以去重操作还会进行排序//相等就放入交集,小的++,大的++ vector<int> ret; set<int>s1(nums1.begin(),nums1.end()); set<int>s2(nums2.begin(),nums2.end());auto it1=s1.begin();auto it2=s2.begin();while(it1!=s1.end()&&it2!=s2.end()){if(*it1<*it2){++it1;}elseif(*it1>*it2){++it2;}else{ret.push_back(*it1);++it2;++it1;}}return ret;}};

找交集:

1.相等就是交集,小的++;小的一定不是交集;
2.返回相等的值就是交集。

拓展:找差集

1.小的就是差集,小的++;
2.相等就同时++
其中一个走到尾部了,另一个没有走完的值也是属于差集。

环形链表

环形链表

在这里插入图片描述


题目解析:
链表中如果有环,返回链表中的环的第一个节点,如果没有环,就返回null.

思路:

问题:放到set里面,为什么要放到set中?

set容器s,用于存储已经访问过的链表节点指针,利用set元素唯一性的特性,来判断节点是否重复,也即是是否进环。

用一个cur指针遍历链表,当链表节点cur没有走到空,
有一个接口count,利用count进行间接的查找。如果找到了返回真,不在返回假。

  1. 在循环体内判断,set 的s中是否存在当前的节点指针cur;
  2. s.count(cur)会返回cur在s中数量(因为set的元素唯一,所以结果是0或者1)如果存在就返回1,返回值是1,说明这个已经被访问过了,链表是存在环的,这个cur指向的值就是环的第一个节点;
  3. 如果s不再当前节点指针,s.count(cur)返回0,就将cur插入到set中,标记为已经访问,然后cur继续向后走继续遍历链表。一直走到cur走到空,结束循环。
  4. 如果走到空了,所有元素都没有二次访问,那就说明该链表没有环,直接返回null.
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */classSolution{public: ListNode *detectCycle(ListNode *head){ set<ListNode*>s; ListNode* cur=head;while(cur){if(s.count(cur))return cur;else{s.insert(cur);cur=cur->next;}}returnnullptr;}};

随机链表的复制

随机链表的复制

在这里插入图片描述


题目解析:
这道题要求构造一个已知链表的深拷贝,将已知链表的nextrandom指针都要指向新的链表中的节点,也就是将已知链表中的构造完完整整拷贝到新的链表中,这里我们用一个map映射就可以解决。

思路

  1. 首先需要有一个map映射,randomMap;构造一个新链表,它的头节点和尾节点分别指向空,然后将原链表中的nextrandom指针分别按照对应的结构拷贝到新链表中;用一个ptr指针来从原链表的头节点一直遍历到尾节点。
  2. 接着开始处理next

copytail==nullptr,说明新链表才刚开始插入,我们就建立新的节点让copyhead=copytail=new Node(ptr->val);
如果拷贝链表中不为空,那么就让copytail->next=new Node(ptr->val);copytail=copytail->next;拷贝新的节点,然后让拷贝节点走向新的位置;需要将拷贝链表和映射randomMap建立关系,这样才好插入,randomMap[ptr]=copytail; 接着ptr不断向后进行遍历。
此时如果ptr走到空,也就是ptr已经遍历完成了原链表。

  1. 现在来处理random节点:

此时copytail一定不为空,但是需要按照原链表的构造完成random的拷贝,让ptr重新进行遍历,然后此时拷贝链表已经有了雏形,给一个copy指针让其从拷贝链表的头节点遍历到尾节点。
如果原链表中ptr指向的random指向为空,那么就让copy的random也是指向为空;
如果不为空呢,就需要建立映射关系,copy->random=randomMap[ptr->random];
copy=copy->next
,ptr=ptr->next,ptrcopy分别向后遍历。
如果ptr走到空节点,遍历完原链表,就结束循环。

  1. 最后就完成了拷贝链表的构造拷贝,返回头节点结束作答。

上代码!

/* // Definition for a Node. class Node { public: int val; Node* next; Node* random; Node(int _val) { val = _val; next = NULL; random = NULL; } }; */classSolution{public: Node*copyRandomList(Node* head){ map<Node*,Node*>randomMap; Node* copyhead=nullptr,*copytail=nullptr; Node* ptr=head;while(ptr){if(copytail==nullptr){copytail=copyhead=newNode(ptr->val);}else{copytail->next=newNode(ptr->val);copytail=copytail->next;} randomMap[ptr]=copytail; ptr=ptr->next;} Node* copy=copyhead; ptr=head;while(ptr){if(ptr->random==nullptr){copy->random=nullptr;}else{copy->random=randomMap[ptr->random];} copy=copy->next; ptr=ptr->next;}return copyhead;}};

前k个高频单词

前k个高频单词

在这里插入图片描述

题目解析
有一个单词表,要返回前k个出现次数最多的单词,这里涉及到统计次数,快速回顾我们之前学过的,什么可以统计次数,countMap,这道题如果有map这块的知识加持,应该会好解答很多,接着看,返回的答案要按照单词的出现频率由高到低排序,涉及排序,是不是要用到sort,又出现一个关键元素,还要按照字典序排序

思路

  1. 要统计每个单词的出现的次数,就要放到map里面进行映射,map<string,int>countMap;用一个迭代器对words进行遍历,for(auto& it:words),countMap[it]++;对每一个出现的元素都进行分析,没有出现过的从默认构造缺省值0++到1;出现过的再++1;直到遍历完所有的元素。

然后需要排序,map本身不支持直接排序,所以将其转换为vector后更灵活。为了同时存储“单词”和它的出现"次数“,将这两个信息绑在一起,可以将单词(string类型)和次数(int类型)封装在一个整体中,pair<string,int>,

  1. vector<pair<string,int>> v (count.begin(),count.end());然后对容器v进行初始化,用的是countMap.begin()countMap.end()
  2. 将数据放到vector中用一个稳定的排序就可以实现上面特殊要求,但是sort底层是快排,是不稳定的,所以我们要用stable_sort,他是稳定的.
  3. 接着排序的时候,这里要实现自定义排序,也就是出现频率由高到低进行排序,**stable_sort的参数除了传迭代器的起始地址,还要额外加一个仿函数的对象,这个仿函数就是用来实现将单词的出现频率由高到低进行排序,**再定义一个结构体
  4. 这个结构体kvFunction中重载operator[]对出现的单词进行排序,要传的不仅仅是单词,还有出现的频率,所以这里再次用pair进行封装,bool operator()(const pair<string,int>w1,const pair<string,int>w2),返回的应该是second,如果w1的出现频率比w2高,那么就让w1排在前面,迭代器中first代表单词,second才是代表出现的频率。 return w1.second>w2.second;
  5. 最后关键一步,打印出前k个出现频率最高的单词,这里我们用vector容器ret对结果进行封装,对前k个单词尾插进入ret,ret.push_back(v[i].first).first就是指单词。

有一个问题:那么这道题是怎么按照字典序排序的呢?
1.借助map的天然字典序特性,代码中首先使用map<string,int>countMap;统计单词出现的频率,map是基于红黑树实现的,会自动按照键的字典序升序。
2.借助stable_sort的稳定型:当两个元素的排序频率相同的时候,会保持他们在排序前的(也就是map中的字典序)相对顺序。
所以map的字典序存储+stable_sort的稳定性,共同保证了**”频率降序,频率相同则字典序升序“**的排序规则。

classSolution{public:structkvFunction{booloperator()(const pair<string,int>w1,const pair<string,int>w2){return w1.second>w2.second;//表示次数多的排在前面}}; vector<string>topKFrequent(vector<string>& words,int k){ map<string,int>countMap;for(auto& it:words){countMap[it]++;}//统计每一个单词出现的次数 vector<pair<string,int>>v(countMap.begin(),countMap.end());//将一个键值对转换为一个vector容器,方便后序排序stable_sort(v.begin(),v.end(),kvFunction());//稳定排序(出现次数相同,保持原map中的字典序) vector<string>ret;for(int i=0;i<k;i++){ret.push_back(v[i].first);}return ret;}};

set和map的构造对比:

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

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

在这里插入图片描述


我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=69rzpw876xc

Read more

PHP通过 trace_id 追踪全链路的庖丁解牛

PHP 通过 trace_id 实现全链路追踪(Distributed Tracing),是将一次用户请求在多个服务(Nginx、PHP-FPM、MySQL、Redis、第三方 API) 的核心机制。 它让工程师从“日志大海捞针”升级为“一键穿透故障”,是高可用系统必备能力。 一、核心原理:trace_id 如何串联全链路? 1. 分布式追踪三要素 元素作用示例trace_id唯一标识一次完整请求a1b2c3d4-...span_id标识链路中的一个操作(如 SQL 查询)e5f6g7h8parent_span_id标识父操作(构建调用树)a1b2c3d4 2. 传递机制:上下文透传 * HTTP 层: * 入口:Nginx 生成 trace_id → 透传给

By Ne0inhk
Flutter 组件 dart_nostr 适配鸿蒙 HarmonyOS 实战:去中心化通讯,构建分布式 Relay 订阅与非对称加密架构

Flutter 组件 dart_nostr 适配鸿蒙 HarmonyOS 实战:去中心化通讯,构建分布式 Relay 订阅与非对称加密架构

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 组件 dart_nostr 适配鸿蒙 HarmonyOS 实战:去中心化通讯,构建分布式 Relay 订阅与非对称加密架构 前言 在鸿蒙(OpenHarmony)生态迈向万物智联、涉及去中心化社交(DeSo)、分布式身份(DID)及抵御单点崩溃的通讯环境背景下,如何构建一套不依赖中心化服务器、具备绝对抗审查性且数据主权归属于用户的通讯协议,已成为决定新一代互联网应用“生命力”的关键。在鸿蒙设备这类强调分布式软总线与端侧安全治理的环境下,如果应用依然依赖脆弱的中心化中转机,由于由于网络链路的单一性,极易由于由于“中心节点宕机”导致全球范围内的业务中断。 我们需要一种能够基于简单非对称加密、支持全球 Relay(中继器)分发且具备“无主化”特性的抗毁协议。 dart_nostr 为 Flutter 开发者引入了 Nostr(Notes

By Ne0inhk
【MySQL数据库基础】(五)MySQL 数据类型深度解析:选对类型 = 性能拉满!

【MySQL数据库基础】(五)MySQL 数据类型深度解析:选对类型 = 性能拉满!

前言         在 MySQL 表结构设计中,数据类型的选择是最核心也最容易踩坑的环节。很多开发者随手给字段设为int、varchar(255),看似省事,实则会导致磁盘空间浪费、查询效率低下,甚至出现数据溢出、精度丢失的问题。         选对数据类型的本质,是用最小的存储空间存储符合业务需求的数据,这不仅能节省服务器资源,还能提升索引和查询的效率。本文将从 MySQL 的四大核心数据类型(数值、字符串、日期时间、枚举集合)出发,结合实战案例讲透每种类型的用法、边界、坑点,还有不同场景下的选择技巧,让你从根源上做好表结构设计!下面就让我们正式开始吧! 一、数据类型总览:四大类覆盖所有业务场景         MySQL 提供了丰富的数据类型,按用途可分为数值类型、字符串类型、日期时间类型和特殊字符串类型(ENUM/SET),不同类型对应不同的存储规则和业务场景,核心设计原则是按需选择,宁小勿大。         先看一张核心数据类型分类表,快速建立整体认知: 分类核心类型适用场景数值类型TINYINT/INT/BIGINT/FLOAT/

By Ne0inhk
基于多智能体强化学习的医疗检索增强生成系统研究—MMOA-RAG架构设计与实现

基于多智能体强化学习的医疗检索增强生成系统研究—MMOA-RAG架构设计与实现

1. 引言 医疗AI面临知识更新快(每年PubMed新增100万文献)、专业性强(SNOMED CT含35万临床概念)等挑战。传统RAG系统存在三大局限: 1. 模块目标冲突(检索高召回率 vs 生成高准确性) 2. 动态依赖缺失(查询改写影响检索策略) 3. 医疗合规风险(FDA要求Class II设备错误率<7%) 本研究特点: * 提出四智能体协同架构(查询/检索/过滤/生成) * 设计临床奖励函数 Rclinical=0.6F1+0.3Safety+0.1ExpertR_{clinical}=0.6F_1+0.3Safety+0.1ExpertR

By Ne0inhk