前言:
本文主要讲解的时对于map与set容器的初步使用,希望大家对map与set容器不熟悉的看了之后可以快速运用set与map到日常中来。(本文适合对vector等基础容器有一定基础的同学)
一、set与map容器常见接口
迭代器接口与以往的所有容器一样,同样的构造方法,同样的名称。
empty判断容器是否为空,size返回当前容器的大小,max_size返回容器的最大大小是多少,我们常用的是前两个接口,max_size一般情况下不会接触。
值得一提是map容器的operator[],这个函数在调用时服用了insert的功能,当当前key值的元素不存在时,会插入一个当前键值元素,并返回新插入的元素地址。如果存在该key值,就返回该key值的地址。
二者都用insert函数来插入,在学了右值引用后,也可以用emplace来进行插入工作(操作与insert一样)。
之后的clear清空容器,swap交换两个容器内容,erase删除某个元素的操作都与其他容器差不多,包括find查找,count判断有无。要注意的是二者的范围查找接口,
对于二者的lower_bound(key);
都会返回第一个不小于 key
的迭代器。upper_bound(key);
都会返回第一个大于 key
的迭代器。
其实,在日常刷题做题中,我们的常用操作就是新建一个set或者mao容器,然后根据题目规则存储信息(set太棒用于去重排序,map用来记录两种数据之间的关联)在使用map时,我们也不会特地使用insert来进行插入,一般会直接在容器名后面[]键值,自动进行插入或者修改操作。
多说没用,实践才是最好的老师,接下来就让我带领大家做几道题应用一下map与set吧。
二 、实战运用
1、随机链表的复制
(题目链接:https://leetcode.cn/problems/copy-list-with-random-pointer/description/)
在没有学习map与set之前,我们是怎么解决这道题的呢?
对于这道题来讲,难点在于怎么深拷贝random 所指向的节点因为我们没有与之对应的映射关系。
以例1为例,当我们拷贝了一个新链表之后,新的13节点的random指针所指向的 7,我们是没有存储的信息的,也就是找不到新链表上的7的地址。为了解决这个问题,我们将新链表插入在老链表之间,比如老1,13之间夹着一个新7.通过让next指向新拷贝的结点,让二者产生联系。在分开链表之间,只需要通过pcur->next->random=pcur->random->next就能得到解决。
class Solution
{
public:Node* copyRandomList(Node* head) {if (head == nullptr){return nullptr;}Node* cur = head;while (cur)//先复制节点并连接点原本链表中{Node* pcur = new Node(cur->val);pcur->next = cur->next;cur->next = pcur;cur = pcur->next;}cur = head;while (cur != nullptr){if (cur->random != nullptr){cur->next->random = cur->random->next;}else{cur->next->random = nullptr;}cur = cur->next->next;}Node* phead = head->next, * prew = head;cur = phead;while (cur){prew->next = cur->next;prew = prew->next;if (prew == nullptr){cur->next = nullptr;cur = cur->next;}else{cur->next = prew->next;cur = cur->next;}}return phead;}
};
但是在有了map之后,就不用这么麻烦。只需要新建一个map,key为老节点,val为新的对应节点,就可以轻松完成代码。
class Solution
{
public:Node* copyRandomList(Node* head){map<Node*,Node*>Map;Node*cur=head,*copyhead=nullptr,*copytail=nullptr;while(cur)//cur遍历原链表进行拷贝并且把原链表与拷贝链表一一插入进map{if(copytail==nullptr){copyhead=copytail=new Node(cur->val);}else{copytail->next=new Node(cur->val);copytail=copytail->next;}Map[cur]=copytail;cur=cur->next;}cur=head;Node* copy=copyhead;while(cur){if(cur->random==nullptr){copy->random=nullptr;}else{copy->random=Map[cur->random];//直接从map表中找到相应的节点地址}cur=cur->next;copy=copy->next;}return copyhead;}
};
2、环形链表2
(题目链接:https://leetcode.cn/problems/linked-list-cycle-ii/solutions/441131/huan-xing-lian-biao-ii-by-leetcode-solution/)
对于该题,在学习set与map之前我们通常用快慢指针法解决,由于数学原理与图形关系,倘若有环,快慢指针必定相遇 ,因此可以反向求出第一个入环节点的位置。
ListNode* hasCycle(struct ListNode* head)
{if (head == NULL || head->next == NULL)//当传过来的链表是空链表,或者传来的只有一个节点,且指向为空,说明不是环{return NULL;//返回false}ListNode* pcur = head, * ptail = head;while (ptail->next && ptail->next->next)//ptail->next与ptail->next->next其中有一个为假,就说明该指针有尽头,不是环{ptail = ptail->next->next;//快慢指针,如果相遇了,说明必有环,不相遇则一定有尽头pcur = pcur->next;if (ptail == pcur){return ptail;//返回相遇点地址}}return NULL;
}struct ListNode* detectCycle(struct ListNode* head){//如果有环,先找到相遇点ListNode* meet = hasCycle(head);if (meet == NULL){return NULL;}//说明有环//快指针速度为慢指针两倍,快指针所走的路程为慢指针两倍,画出路程图可以列出关系式:当fast从相遇点出发,slow从head出发,速度相同,路程相同//会同时到达第一个相交节点ListNode* slow = head;while (slow != meet){slow = slow->next;meet = meet->next;}return meet;}
但我们现在可以用set来记录出现过的链表节点,轻松解决问题。
class Solution
{
public:ListNode *detectCycle(ListNode *head) {set<ListNode *>Set;ListNode*ret=nullptr,*cur=head;while(cur){bool Find=Set.insert(cur).second;if(Find==false){ret=cur;break;}cur=cur->next;}return ret;}
};
二者代码复杂度一看便知。
3、前K个高频单词
(题目链接:https://leetcode.cn/problems/top-k-frequent-words/description/)
在我们看见第k大,前k个这样的关键字时,我们就想到了这是一个topk问题,自然也可以用优先级队列解决。但我们这里可以结合map的性质,与仿函数相结合解决问题。
我们可以用一个一个 map<string, int> Map
,用于统计每个单词的出现次数。string
是单词,int
是该单词的出现频率。
随后使用 for (auto it : words)
遍历 words
数组,对于每个单词,将其频率在 Map
中加 1
。这个循环完成后,Map
中存储了每个单词对应的出现频率。
接下来再缔造一个vector<pair<string, int>>,用来我们对pair进行排序,所以我们要添加仿函数来排序pair对象。
class Solution
{
public:struct Compare{bool operator()(const pair<string, int>& a, const pair<string, int>& b){/*return a.second > b.second || (a.second == b.second && a.first < b.first);*/return a.second > b.second ;}};vector<string> topKFrequent(vector<string>& words, int k){map<string, int>Map;vector<string>ret;for (auto it : words){Map[it]++;}vector<pair<string, int>>arr(Map.begin(), Map.end());/* sort(arr.begin(), arr.end(), Compare());*/stable_sort(arr.begin(), arr.end(), Compare());for (int i = 0; i < k; ++i){ret.push_back(arr[i].first);}return ret;}
};
我这里的代码结合了map本身对key键值对就进行了字典序排序,所以我们后面才使用稳定的stable_sort,这样就算两个key的val相同,二者的字典序相对顺序也不会改变。。如果我们要使用sort,就需要在仿函数中加上对val值相同,比较字典序的判断条件。
4、单词识别
(题目链接;http://https://www.nowcoder.com/practice/http://https://www.nowcoder.com/practice/)
对与此题,我们也可以类似的用map来存储单词与出现频率的关系。
为了能够接受一句话,所以我们要用getline来接受而不是cin。
#include <iostream>
using namespace std;
#include<map>
#include<algorithm>
#include<string>
#include<vector>struct Compare
{bool operator()(const pair<string,int>&a,const pair<string,int>&b){return a.second>b.second;}
};int main()
{string s;map<string,int>Map;getline(cin,s);char*ch;for(int i=0;i<s.size();++i){string ss;while(s[i]!=' '){if(s[i]=='.'){break;}ss+=tolower(s[i++]);}Map[ss]++;}vector<pair<string,int>>arr(Map.begin(),Map.end());stable_sort(arr.begin(),arr.end(),Compare());for(auto it:arr){cout<<it.first<<":"<<it.second<<endl;}return 0;
}
三、总结
在本文中,我们通过多个实际例题,深入探讨了 map
和 set
容器的常见操作和实际应用。这些容器在处理数据的过程中能够提供强大的支持,尤其在涉及到元素的唯一性、快速查找和数据关联的场景下。
1. 基本操作与接口
我们首先回顾了 map
和 set
容器的基本操作,包括元素插入、查找、删除等常用接口。在日常的算法题解中,这些基本操作为我们提供了便捷的工具。例如,使用 map
记录元素之间的关联关系,利用 set
处理去重和排序问题。
2. 实战应用
我们通过几个经典的算法题,展示了如何在实际编程中运用 map
和 set
容器。通过这些案例,我们看到了这些容器在不同场景下的灵活性和高效性:
- 随机链表的复制:利用
map
建立新旧节点之间的映射关系,简化了链表的深拷贝操作。 - 环形链表检测:通过
set
记录已经访问过的节点,轻松找到链表中的环。 - 前K个高频单词:结合
map
统计频率和仿函数排序,快速找到出现频率最高的单词。 - 单词识别:使用
map
统计句子中每个单词的频率,并按出现次数进行排序,展示了map
的实用性。
3. 稳定排序的应用
在排序过程中,我们强调了 stable_sort
的使用,这对于保持排序的稳定性尤为重要。在某些题目中,我们需要确保排序过程中,相同频率的单词按字典序排列,而 stable_sort
正是保证这种稳定性的关键。
4. 代码复杂度与性能
通过与传统方法的比较,我们可以看到使用 map
和 set
后,代码复杂度得到了简化,逻辑更为清晰。虽然这些容器的底层实现较为复杂,但它们提供的高效查找和插入性能,使得我们在解决实际问题时受益匪浅。
结语
学习 map
和 set
容器不仅仅是掌握其基本用法,更重要的是通过实践,理解它们在不同场景中的应用和优势。本文的示例和解析,希望能帮助读者更好地掌握这些工具,并在今后的算法和编程实践中更加得心应手。通过不断练习,读者可以熟练地将 map
和 set
应用于各种复杂的数据处理任务中,从而提升编程效率和代码质量。