前言:
本文主要讲解的时对于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
应用于各种复杂的数据处理任务中,从而提升编程效率和代码质量。