目录
1、map的简述
2、map的使用
2.1 insert
2.2 operator*、operator->
2.3 operator[]
3、multimap
1、map的简述
map与set一样是关联式容器
map就相当于二叉搜索树中的KV模型,底层是使用红黑树实现的,仿函数默认是less,即比根小的往左走,比根大的往右走。但是,map中的Key和Value是组合在一起的,是一个新的数据结构pair。pair称为键值对。map中也是不能键值冗余的
2、map的使用
map大部分是和set相似的,这里就介绍几个与set有较大区别的成员函数
2.1 insert
在pair中,两个成员变量称为first和second。注意:map中结点的first是const的,一旦放入,不允许修改
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)
{}
};
所以,向map中插入元素时,不能将两个值分开写,应该将两个值放入一个pair才能放入
void test_map1()
{map<string, string> dict;dict.insert("left", "左边");
}
像上面这样的写法就是错的,下面介绍4种写法
pair中有一个make_pair函数是用来创建一个pair对象的
void test_map1()
{map<string, string> dict;// 1、定义有名对象pair<string, string> kv1("left", "左边");dict.insert(kv1);// 2、定义匿名对象dict.insert(pair<string, string>("right", "右边"));// 3、用make_pair创建一个对象dict.insert(make_pair("insert", "插入"));// 4、用花括号括起来,多参数的隐式类型转换dict.insert({ "string","插入" });
}
map初始化是可以使用迭代器区间初始化的
void test_map2()
{map<string, string> dict = { {"left","右边"},{"right","右边"},{"insert","插入"} };
}
外层的{}是initializer_list,内层的{}是隐式类型转换
2.2 operator*、operator->
pair是不支持流插入和流提取的,所以想要获得map的迭代器中的值不能直接对迭代器解引用
void test_map2()
{map<string, string> dict = { {"left","右边"},{"right","右边"},{"insert","插入"} };map<string, string>::iterator it = dict.begin();while (it != dict.end()){cout << (*it) << " ";++it;}
}
此时cout处是会报错的,因为*it获得的是pair对象,而pair对象又不支持流提取和流插入
void test_map2()
{map<string, string> dict = { {"left","右边"},{"right","右边"},{"insert","插入"} };map<string, string>::iterator it = dict.begin();while (it != dict.end()){cout << (*it).first << "-" << (*it).second << endl;++it;}
}
因为pair里面存的数据类型支持流插入和流提取,结果是,遍历出的顺序与插入顺序是不同的,因为是按Key,即pair的first进行了排序
不过,最方便的其实是使用->来访问pair中的数据
void test_map2()
{map<string, string> dict = { {"left","右边"},{"right","右边"},{"insert","插入"} };map<string, string>::iterator it = dict.begin();while (it != dict.end()){cout << it->first << "-" << it->second << endl;++it;}
}
这里是省略了一个->的,前面也有讲过
迭代器的operator*是返回里面数据的引用,迭代器的operator->是返回里面数据的指针。其实当容器的数据存的是一个结构体时,若要访问迭代器里面的数据,是没办法*it的,需要(*it).xxx,此时更推荐使用opertaor->
可以使用迭代器则一定可以使用范围for
void test_map3()
{map<string, string> dict = { {"left","右边"},{"right","右边"},{"insert","插入"} };map<string, string>::iterator it = dict.begin();for (const auto& e : dict){cout << e.first << "-" << e.second << endl;}for (auto& [x, y] : dict){cout << x << "-" << y << endl;}
}
有两种写法,第一种写法一定要使用引用,防止深拷贝。第二种写法叫做结构化绑定,是C++17的内容,同样要加引用,first给x,second给y
2.3 operator[]
operator[]是map特有的,set中没有
此时,如果我们要统计出一个字符串数组中,每个字符串出现的次数,则可以使用map,insert配合find
void test_map4()
{string arr[] = { "苹果","西瓜","苹果","西瓜","苹果","梨","香蕉" };map<string, int> countTree;for (const auto& str : arr){// 先查找水果在不在搜索树中// 1、不在,说明水果第一次出现,则插入<水果,1>// 2、在,则查找到的结点中水果对应的次数++auto ret = countTree.find(str);if (ret == countTree.end())countTree.insert({ str,1 });elseret->second++;}
}
当然,也可以使用operator[]
void test_map5()
{string arr[] = { "苹果","西瓜","苹果","西瓜","苹果","梨","香蕉" };map<string, int> countTree;for (const auto& str : arr){countTree[str]++;}
}
这里是传入str后,若map中没有,则插入并++,若有,则返回这个Key对应的value,然后对value++
我们重点看最下面哪一行,operator[]底层是调用insert实现的
insert的返回值是一个pair,插入成功,pair的first是新插入元素的迭代器,second是true,插入失败,说明这个key已经存在了,pair的first指向与key相等的迭代器,second位false。所以insert也可以通过返回的pair来判断是否插入成功。插入成功时,这个结点的first用传过来的值,second调用对应类型的默认构造函数。像上面哪里,若这个水果类型还没有,则插入后,second会调用int的默认构造,也就是构造成0,然后再++,就变成1
void test_map6()
{map<string, string> dict;dict["left"] = "左边";
}
像上面这段代码,会发现key为"left"的没有,所以构造一个pair,这个pair的first是"left",second调用string的默认构造,也就是一个空字符串"",然后用这个pair去构造一个map的结点插入
所以,dict[key],若key存在是查找,若不存在是插入,同时还能修改key对应的value。但在用来查找时要注意,若这个值不存在,会变成插入
3、multimap
multimap是允许键值冗余的map
map插入时,只比较key,若插入的key已经存在,则不进行操作,不会修改对应的value
void test_map6()
{map<string, string> dict;dict.insert({ "left","左边" });dict.insert({ "left","右边" });dict.insert({ "left","插入" });
}
像这里,left的value是左边
multimap插入时,无论key是否存在,都插入,并且没有operator[]
其他与map的差别,与set和multiset类似,这里就不过多介绍了
4、例题
学习了set和map后,可以对以前学过的一些题进行更新解法
4.1 复杂链表的复制
在之前,是将每一个结点复制,然后链接到其后面,通过这样的方式建立起拷贝结点与原结点的映射关系,这样拷贝结点就可以通过与原结点的关系来复制random,复制好random后,就可以将两个链表分开,返回拷贝的链表即可
class Solution {
public:Node* copyRandomList(Node* head) {if(head == nullptr) return head;// 在原链表的每一个结点后面加一个复制的结点Node* cur = head;while(cur){Node* copy = new Node(cur->val);copy->next = cur->next;cur->next = copy;cur = copy->next;}// 复制随机指针cur = head;while(cur){Node* copy = cur->next;if(cur->random == nullptr) copy->random = nullptr;else copy->random = cur->random->next;cur = copy->next;}// 分成两个链表Node* copyHead = head->next;cur = head;while(cur){Node* copy = cur->next;Node* next = cur->next->next;if(next)copy->next = next->next;elsecopy->next = nullptr;cur->next = next;cur = next;}return copyHead;}
};
这样是比较麻烦的,我们学习了map后,就可以利用map将原结点与拷贝结点建立关系,map<ListNode*, ListNode*>,这样就可以通过原结点来找到对应的拷贝结点了
class Solution {
public:Node* copyRandomList(Node* head) {Node* copyHead = nullptr,*copyTail = nullptr;Node* cur = head;map<Node*,Node*> OriginalCopy; // 创建一个原来结点与复制节点对应的mapwhile(cur){if(copyHead == nullptr){copyHead = copyTail = new Node(cur->val);}else{copyTail->next = new Node(cur->val);copyTail = copyTail->next;}OriginalCopy[cur] = copyTail; // 这样通过operator[]传入一个原结点,就能返回这个原结点对应的拷贝结点cur = cur->next;}cur = head;copyTail = copyHead;while(cur){if(cur->random == nullptr){copyTail->random = nullptr;}else{copyTail->random = OriginalCopy[cur->random];}cur = cur->next;copyTail = copyTail->next;}return copyHead;}
};
实际上,要用一个值去找另一个值都可以用map
4.2 环形链表
在之前,我们是定义一个快指针和一个慢指针,快指针一次走一步,慢指针一次走两步,若快慢指针在移动过程中能够相遇,则说明有环,若快指针走到nullptr了,说明没环
class Solution {
public:bool hasCycle(ListNode *head) {if(head == nullptr) return false;ListNode* slow = head,*fast = head;while(fast && fast->next){slow = slow->next;fast = fast->next->next;if(slow == fast) return true;}return false;}
};
现在我们可以使用set,用set保存每个结点,若指针走到nullptr了,则没有环,若插入失败,则说明有环,并且插入失败的这个结点,就是入环的结点
class Solution {
public:bool hasCycle(ListNode *head) {set<ListNode*> s;ListNode* cur = head;while(cur){// 也可以auto ret = s.insert(cur);pair<set<ListNode*>::iterator,bool> ret = s.insert(cur);if(ret.second == false) return true;cur = cur->next;}return false;}
};
4.3 环形链表II
在之前,我们是先找到快、慢指针相遇的那个结点,然后定义一个指针从头结点出发,一个指针从相遇结点出发,这两个指针相遇的结点就是入环结点
class Solution {
public:ListNode *detectCycle(ListNode *head) {if(head == nullptr) return nullptr;ListNode* slow = head,*fast = head;while(fast && fast->next){slow = slow->next;fast = fast->next->next;if(slow == fast) break;}if(fast == nullptr || fast->next == nullptr) return nullptr;while(head != slow){head = head->next;slow = slow->next;}return head;}
};
现在我们可以使用set,用set保存每个结点,若指针走到nullptr了,则没有环,若插入失败,则说明有环,并且插入失败的这个结点,就是入环的结点
class Solution {
public:ListNode *detectCycle(ListNode *head) {set<ListNode*> s;ListNode* cur = head;while(cur){auto ret = s.insert(cur);if(ret.second == false) return cur;cur = cur->next;}return nullptr;}
};
4.4 两个数组的交集
首先,我们需要使用两个set对nums1和nums2分别进行排序和去重,然后比较两个set,若两个元素相等,就是交集,放如vector后,两个迭代器同时++,若其中一个迭代器指向的元素比较小,则小的那个迭代器++
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());vector<int> ret;// set排过序,依次比较,若相等则是交集auto it1 = s1.begin();auto it2 = s2.begin();while(it1 != s1.end() && it2 != s2.end()){if(*it1 < *it2) it1++;else if(*it1 > *it2) it2++;else{ret.push_back(*it1);it1++;it2++;}}return ret;}
};
4.5 两个数组的交集II
在上一题,我们只需要向vector中放入交集中的一个,这道题需要放入多个,所以需要使用map
class Solution {
public:vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {map<int,int> m1;map<int,int> m2;vector<int> ret;for(auto e : nums1)m1[e]++;for(auto e : nums2)m2[e]++;auto it1 = m1.begin();auto it2 = m2.begin();while(it1 != m1.end() && it2 != m2.end()){if(it1->first < it2->first) ++it1;else if(it1->first > it2->first) ++it2;else{int x = min(it1->second,it2->second);for(int i = 0;i < x;i++) ret.push_back(it1->first);++it1;++it2;}}return ret;}
};
在云存储中就有利用到计算交集和差集的算法,假设同一个用户有手机和iPad两个设备,并且都开启了云存储,则在手机上或iPad上删除了一个东西,此时是不会真正删除的,过一会当同步服务开启后,会启动计算差集和交集的算法,若手机或iPad中有一个与云存储的数据不一样,则将云存储的数据同步到手机或iPad上。只有去云存储上删除了才是真的删除了。同步服务会计算出一个差集和一个交集。
差集进行同步
交集进行比对:每个文件都会有一个属性:最后修改时间。若修改时间变了,则进行同步。修改是在手机或iPad上,同步至云存储
4.6 前K个高频单词
这道题我们可以使用map<string,int>来存储字符串和这个字符串出现的频次,但是map是根据pair中的first,也就是string来进行排序的,并不是根据频次排序的。所以我们需要额外使用priority_queue或者sort来对map中的频次进行排序。因为这道题数据量不大,所以使用sort为例,数据量大时最好使用priority_queue。因为map的迭代器是随机迭代器,所以不能使用sort。所以使用vector<pair<string, int>>来存储map中的数据,这样就可以实现排序了。但是sort默认是根据vector中存储的对象进行排序,也就是根据pair来进行排序,pair是支持比较大小的,两个pair对象实现比较first,first相等再比较second
我们在这里需要的只是比较pair对象的second即可,并且是排降序,所以使用仿函数
class Solution {
public:struct Compare{bool operator()(const pair<string,int>& x,const pair<string,int>& y){return x.second > y.second;}};vector<string> topKFrequent(vector<string>& words, int k) {map<string, int> countMap;for(auto e : words) countMap[e]++;vector<pair<string, int>> v(countMap.begin(),countMap.end());// 降序sort(v.begin(),v.end(),Compare()); // sort需要传一个仿函数的对象,这里使用匿名对象vector<string> ret;for(int i = 0;i < k;i++){ret.push_back(v[i].first);}return ret;}
};
但这个代码只能过一部分的测试用例
此时我们会发现,找出来的k个字符串是一样的,只是排列的顺序不同导致错误
题目中有:返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。
也就是说,我们找对了,只是因为没有将出现频次相同的字符串按字典序排序输出。我们可以将排序完的vector输出出来看一下
class Solution {
public:struct Compare{bool operator()(const pair<string,int>& x,const pair<string,int>& y){return x.second > y.second;}};vector<string> topKFrequent(vector<string>& words, int k) {map<string, int> countMap;for(auto e : words) countMap[e]++;vector<pair<string, int>> v(countMap.begin(),countMap.end());// 降序sort(v.begin(),v.end(),Compare()); // sort需要传一个仿函数的对象,这里使用匿名对象for(const auto& e : v){cout<<e.first<<":"<<e.second<<endl;}vector<string> ret;for(int i = 0;i < k;i++){ret.push_back(v[i].first);}return ret;}
};
会发现确实是因为没有将出现频次相同的字符串按字典序排序输出
在将字符串和出现频次放入map中完成时,同一个频次的字符是按字典序排序的,并且vector刚被初始化好时,相同频次的字符串也是按字典序排序的。之所以sort完了之后,相同频次的字符串就不是按字典序的顺序排序是因为sort不是稳定的排序,只有稳定的排序才能保证相同的值的相对顺序不变。头文件algorithm中,有stable_sort是稳定的排序,可以使用这个替代sort
class Solution {
public:struct Compare{bool operator()(const pair<string,int>& x,const pair<string,int>& y){return x.second > y.second;}};vector<string> topKFrequent(vector<string>& words, int k) {map<string, int> countMap;for(auto e : words) countMap[e]++;vector<pair<string, int>> v(countMap.begin(),countMap.end());// 降序stable_sort(v.begin(),v.end(),Compare()); // sort需要传一个仿函数的对象,这里使用匿名对象vector<string> ret;for(int i = 0;i < k;i++){ret.push_back(v[i].first);}return ret;}
};
这样就可以了,当然,我们也可以仍然使用sort,只是需要修改一下仿函数,当出现频次不同时,按出现频次排序,当出现频次相同时,按string的字典序升序排序。并且若说使用优先级队列只能用这种方法,因为优先级队列没有稳定不稳定一说
class Solution {
public:struct Compare{bool operator()(const pair<string,int>& x,const pair<string,int>& y){return x.second > y.second || (x.second == y.second && x.first < y.first);}};vector<string> topKFrequent(vector<string>& words, int k) {map<string, int> countMap;for(auto e : words) countMap[e]++;vector<pair<string, int>> v(countMap.begin(),countMap.end());// 降序sort(v.begin(),v.end(),Compare()); // sort需要传一个仿函数的对象,这里使用匿名对象vector<string> ret;for(int i = 0;i < k;i++){ret.push_back(v[i].first);}return ret;}
};