C++--map和set的使用
map和set的使用
- map和set的使用
- 1. 基础概念
- 1.1 序列式容器和关联式容器
- 1.2 键值对pair
- 1.3 树状结构和哈希结构
- 2. set
- 2.1 set的介绍
- 2.2 set 的使用
- 2.2.1 构造
- 2.2.2 迭代器
- 2.2.3 修改
- 2.2.4 操作
- 3. multiset
- 3.1 multiset 的介绍
- 3.2 multiset 的使用
- 5. 有关 set 的面试题
- 5.1 两个数组的交集
- 5.2 环形链表 II
- 6. map
- 6.1 map 的介绍
- 6.2 map 的使用
- 6.2.1 构造
- 6.2.2 迭代器
- 6.2.3 修改
- 6.2.4 操作
- 6.2.5 元素访问
- 6.2.5.1 函数原型
- 6.2.5.2 函数定义
- 5.2.5.3 内部实现
- 6.2.5.4 代码示例
- 7. multimap
- 8. 有关 map 的面试题
- 8.1 随机链表的复制
- 8.2 前K个高频单词
map和set的使用
1. 基础概念
1.1 序列式容器和关联式容器
C++STL包含了序列式容器和关联式容器:
- 序列式容器里面存储的是元素本身,其逻辑结构为线性序列的数据结构,两个位置储存的值之间一般没有紧密的关联关系,比如交换一下依旧是序列式容器。比如:vector,list,deque,forward_list(C++11)等。
- 关联式容器里面存储的是 <key, value> 结构的键值对,其逻辑结构通常为非线性序列的数据结构,两个位置存储的值有紧密关联关系,交换一下存储结构就会别破坏,但是在数据检索时比序列式容器效率更高。比如:set、map、unordered_set、unordered_map等。
注意: C++STL 当中的 stack、queue 和 priority_queue 属于容器适配器,它们默认使用的基础容器分别是 deque、deque 和 vector。
1.2 键值对pair
pair
键值对是用来表示具有一一对应关系的一种结构,该结构中一般只包含两个成员变量:key 和 value;其中 key 代表键值,value 代表与 key 对应的信息。
在上一节学习二叉搜索树的时候提到的 key-value 模型中的 key-value 其实就是键值对。
可以用上一节中提到的英汉互译的例子来理解 key-value 键值对,现在要建立一个英汉互译的字典,那该字典中必然有英文单词与其对应的中文含义,而且,英文单词与其中文含义是一一对应的关系,即通过该应该单词,在词典中就可以找到与其对应的中文含义。
STL 中键值对的定义如下:(SGI 版本)
template <class T1, class T2>
struct pair
{typedef T1 first_type;typedef T2 second_type;T1 first; //keyT2 second; //valuepair() : first(T1()), second(T2()) //默认构造{}pair(const T1& a, const T2& b) : first(a), second(b){}
};
可以看到,C++ 中的键值对是通过 pair 类来表示的,pair 类中的 first 就是键值 key,second 就是 key 对应的信息 value;那么以后我们在设计 key-value 模型的容器时只需要在容器/容器的每一个节点中定义一个 pair 对象即可.
这里可能会有疑问,为什么不直接在容器中定义 key 和 value 变量,而是将 key、value 合并到 pair 中整体作为一个类型来使用呢?
这是因为 C++ 一次只能返回一个值,如果将 key 和 value 单独定义在容器中,那么就无法同时返回 key 和 value;而如果将 key、value 定义到另一个类中,那就可以直接返回 pair,然后再到 pair 中分别去取 first 和 second 即可。
make_pair 函数
由于 pair 是类模板,所以通常是以 显式实例化 + 匿名对象 的方式来进行使用,但是由于显式实例化比较麻烦,所以 C++ 还提供了 make_pair 函数,其定义如下:
template <class T1, class T2>
pair<T1, T2> make_pair(T1 x, T2 y)
{return (pair<T1, T2>(x, y));
}
如上,make_pair 返回的是一个 pair 的匿名对象,匿名对象会自动调用 pair 的默认构造完成初始化;但由于 make_pair 是一个函数模板,所以模板参数的类型可以根据实参来自动推导完成隐式实例化,这样就不用每次都显式指明参数类型了。
注:由于 make_pair 使用起来比 pair 方便很多,所以一般都是直接使用 make_pair,而不使用 pair。
1.3 树状结构和哈希结构
根据应用场景的不同,C++ STL 总共实现了两种不同结构的关联式容器:树型结构和哈希结构。
类联式容器 | 容器结构 | 底层实现 |
---|---|---|
set, map, multiset, multimap | 树型结构 | 平衡搜索树(红黑树) |
unordered_set, unordered_map, unordered_multiset, unordered_multimap | 哈希结构 | 哈希表 |
其中,树型结构容器中的元素是一个有序的序列,而哈希结构容器中的元素是一个无序的序列。
2. set
2.1 set的介绍
set 是按照一定次序存储元素的容器,其底层是一棵平衡二叉搜索树 (红黑树),由于二叉搜索树的每个节点的值满足左孩子 < 根 < 右孩子,并且二叉搜索树中没有重复的节点,所以 set 可以用来排序、去重和查找,同时由于这是一棵平衡树,所以 set 查找的时间复杂度为 O(logN),效率非常高。
同时,set 是一种 key 模型 的容器,也就是说,set 中只有键值 key,而没有对应的 value,并且每个 key 都是唯一的 。set 中的元素也不允许修改,因为这可能会破坏搜索树的结构,但是 set 允许插入和删除。
下面是 set 的声明:
emplate < class T, // set::key_type/value_typeclass Compare = less<T>, // set::key_compare/value_compareclass Alloc = allocator<T> // set::allocator_type> class set;
总结:
- set 是 key 模型的容器,所以插入元素时,只需要插入 key 即可,不需要构造键值对。
- set 中的元素不可重复,因此可以使用 set 进行去重。
- 由于 set 底层是红黑树,所以使用 set 的迭代器遍历 set 中的元素,可以得到有序序列,即 set 可以用来排序。
- set 默认使用的比较函数是 less,因此 set 中的元素默认按照小于来比较。如果不支持或者想按自己的需求可以自行实现仿函数传给第二个模版参数。
- set 底层存储数据是从空间适配器申请的,如果需要自己实现内存池,可以传给第三个参数。
- set 声明中的的 T 就是底层关键字类型也就是 key ,并且一般情况下对于后面两个模版参数都是不需要手动传的。
- 由于 set 底层是平衡树结构,所以 set 中查找某个元素,时间复杂度为 O(logN)。
- set 中的元素不可修改,因为这可能破坏搜索树的结构。
- set 中的底层是平衡二叉搜索树(红黑树)来实现。
2.2 set 的使用
2.2.1 构造
和传统容器一样,set 也支持单个元素构造、迭代器区间构造以及拷贝构造:
// empty (1) 无参默认构造
explicit set (const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());// range (2) 迭代器区间构造
template <class InputIterator>
set (InputIterator first, InputIterator last,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());// copy (3) 拷贝构造
set (const set& x);// initializer list (5) initializer 列表构造
set (initializer_list<value_type> il,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
2.2.2 迭代器
set的支持正向和反向迭代,遍历默认按升序排序,因为底层是二叉搜索树,迭代器遍历走的是中序;支持迭代器就意味着支持范围for,set的iterator和const_iterator都不支持迭代器修改数据,修改关键字数据,破坏了底层搜索树的结构。
// 迭代器是一个双向迭代器
iterator -> a bidirectional iterator to const value_type// 正向迭代器
iterator begin();
iterator end();// 反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();
2.2.3 修改
set 有如下修改操作:
// Member types
key_type -> The first template parameter (T)
value_type -> The first template parameter (T)// 单个数据插入,如果已存在则插入失败
pair<iterator,bool> insert (const value_type& val);// 列表插入,已经在容器中存在的值不会插入
void insert (initializer_list<value_type> il);// 迭代器区间插入,已经在容器中存在的值不会插入
template <class InputIterator>
void insert (InputIterator first, InputIterator last);// 删除一个迭代器位置的值
iterator erase (const_iterator position);// 删除val,val不存不存在返回0,存在返回1
size_type erase (const value_type& val);// 删除一段迭代器区间的值
iterator erase (const_iterator first, const_iterator last);
其中 swap 就是交换两棵树的根,clear 就是释放树中的所有节点,最重要的修改操作是 insert 和 erase。
insert
insert 支持插入一个值、在某个迭代器位置插入值、插入一段迭代器区间,我们学会第一个即可,插入的过程就是二叉搜索树的插入过程;需要注意的是 insert 的返回值是 pair 类型,pair 中第一个元素代表插入的迭代器位置,第二个元素代表是否插入成功 (插入重复节点会返回 false):
#include<iostream>
#include<set>
using namespace std;int main()
{// 去重+升序排序set<int> s;// 去重+降序排序(给一个大于的仿函数)// set<int, greater<int>> s;s.insert(5);s.insert(2);s.insert(7);s.insert(5);//set<int>::iterator it = s.begin();auto it = s.begin();while (it != s.end()){// error C3892: “it”: 不能给常量赋值// *it = 1;cout << *it << " ";++it;}cout << endl;// 插入一段initializer_list到表值,已经存在的值插入失败s.insert({ 2, 8, 3, 9 });for (auto e : s){cout << e << " ";}cout << endl;set<string> strset = { "sort", "insert", "add" };// 遍历string比较ascII码大小顺序遍历的for (auto e : strset){cout << e << " ";}cout << endl;
}
erase
erase 也有三种,常用的是第一种和第二种,删除指定键值的数据和删除指定迭代器位置的数据:
#include<iostream>
#include<set>
using namespace std;int main()
{// 插入数据set<int> s = { 4,2,7,2,8,5,9 };for (auto e : s){cout << e << " ";}cout << endl;// 删除最⼩值 s.erase(s.begin());for (auto e : s){cout << e << " ";}cout << endl;// 直接删除x int x;cin >> x;int num = s.erase(x);if (num == 0){cout << x << "不存在!" << endl;}for (auto e : s){cout << e << " ";}cout << endl;// 直接查找在利⽤迭代器删除x cin >> x;auto pos = s.find(x);if (pos != s.end()){s.erase(pos);}else{cout << x << "不存在!" << endl;}for (auto e : s){cout << e << " ";}cout << endl;return 0;
}
2.2.4 操作
set 还有一些其他操作相关的函数:
// 查找val,返回val所在的迭代器,没有找到返回end()
iterator find (const value_type& val);// 查找val,返回val的个数
size_type count (const value_type& val) const;// 返回val不等于val量的迭代器
iterator lower_bound (const value_type& val) const;// 返回大于val位置的迭代器
iterator upper_bound (const value_type& val) const;
find
其中比较重要的只有 find,find 的作用是在搜索树中查找 key 对应的节点,然后返回节点位置的迭代器,如果找不到,find 会返回 end():
以上介绍的 find 是 set 容器中自己实现的 find 函数,但是在之前容器的学习中可以知道算法库中也有一个 find 函数,set 也不例外但是很少使用,因为效率太低。
// 算法库的查找 O(N) -- 不会这样使用
auto pos1 = find(s.begin(), s.end(), x);
// set⾃⾝实现的查找 O(logN)
auto pos2 = s.find(x);
count
由于 set 中不允许出现相同的 key,因此在 set 中 count 函数的返回值只有1 或者 0,可以说没有什么价值,但是有时候count也可以间接实现快速查找的作用,set 中定义 count 主要是因为 count 在 multiset 中有作用,这里是为了保持一致。
#include<iostream>
#include<set>
using namespace std;int main()
{set<int> s = { 4,2,7,2,8,5,9 };for (auto e : s){cout << e << " ";}cout << endl;// 利⽤count间接实现快速查找 int x;cin >> x;if (s.count(x)){cout << x << "在!" << endl;}else{cout << x << "不存在!" << endl;}return 0;
}
lower_bound 和 upper_bound
lower_bound 和 upper_bound 是得到一个左闭右开的迭代器区间,然后可以对这段区间进行某些操作。
因为 STL 容器规定了迭代器区间必须是左闭右开,所以例如下面在删除这串数字就可能出现问题。因为是左闭右开,所以查找左边的值的时候需要返回大于等于30的值,但是在查找右边的值的时候规定右开,所以在查找60的时候找到了60是不可以返回60的而需要返回70,如果没有找到60需要返回比60大的第一个值。而lower_bound 和 upper_bound就可以解决这个问题。
lower_bound 是找大于等于这个值,相当于左闭。
upper_bound 是找大于的这个值,相当于右开。
#include<iostream>
#include<set>
using namespace std;int main()
{std::set<int> myset;for (int i = 1; i < 10; i++)myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90for (auto e : myset){cout << e << " ";}cout << endl;// 实现查找到的[itlow,itup)包含[30, 60]区间 // 返回 >= 30 auto itlow = myset.lower_bound(30);// 返回 > 60 auto itup = myset.upper_bound(60);// 删除这段区间的值 myset.erase(itlow, itup);for (auto e : myset){cout << e << " ";}cout << endl;return 0;
}
3. multiset
3.1 multiset 的介绍
multiset 也是 key 模型 的容器,它和 set 唯一的区别在于 multiset 中允许存在重复的 key 值节点,所以 multiset 可以用来排序和查找,但是不能用来去重。
3.2 multiset 的使用
multiset 和 set 的使用基本完全类似,主要区别点在于 multiset 支持值冗余,那么 insert/find/count/erase 都围绕着支持值冗余有所差异,具体参看下面的样例代码理解。
排序:
#include<iostream>
#include<set>
using namespace std;int main()
{// 相⽐set不同的是,multiset是排序,但是不去重 multiset<int> s = { 4,2,7,2,4,8,4,5,4,9 };auto it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;return 0;
}
查找:
#include<iostream>
#include<set>
using namespace std;int main()
{// 相⽐set不同的是,multiset是排序,但是不去重 multiset<int> s = { 4,2,7,2,4,8,4,5,4,9 };auto it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;// 相⽐set不同的是,x可能会存在多个,find查找中序的第⼀个 int x;cin >> x;auto pos = s.find(x);//循环查找while (pos != s.end() && *pos == x){cout << *pos << " ";++pos;}cout << endl;return 0;
}
计数:
#include<iostream>
#include<set>
using namespace std;int main()
{// 相⽐set不同的是,multiset是排序,但是不去重 multiset<int> s = { 4,2,7,2,4,8,4,5,4,9 };auto it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;// 相⽐set不同的是,count会返回x的实际个数 int x;cin >> x;cout << s.count(x) << endl;return 0;
}
删除:
#include<iostream>
#include<set>
using namespace std;int main()
{// 相⽐set不同的是,multiset是排序,但是不去重 multiset<int> s = { 4,2,7,2,4,8,4,5,4,9 };auto it = s.begin();while (it != s.end()){cout << *it << " ";++it;}cout << endl;// 相⽐set不同的是,erase给值时会删除所有的x int x;cin >> x;s.erase(x);for (auto e : s){cout << e << " ";}cout << endl;return 0;
}
5. 有关 set 的面试题
5.1 两个数组的交集
题目链接:349. 两个数组的交集 - 力扣(LeetCode)
题目描述:给定两个数组
nums1
和nums2
,返回 它们的 交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。
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());// 因为set遍历是有序的,有序值,依次⽐较 // 小的++,相等的就是交集 vector<int> ret;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;}
};
解题思路:
这段代码的核心思想是利用 set
(集合) 这个数据结构的特性来高效地找出两个数组的交集。set
有两个关键特性被运用在这里:
- 自动去重: 当你将一个数组的元素插入到
set
中时,重复的元素会自动被忽略。 - 自动排序:
set
中的元素总是保持有序状态。
代码利用这两个特性,首先将两个输入的数组 nums1
和 nums2
转换成两个有序且无重复元素的集合 s1
和 s2
。然后,它使用一种叫做“双指针”的技巧来高效地比较这两个有序集合,找出它们的共同元素。实现找到两段数据的交集。
5.2 环形链表 II
题目链表:142. 环形链表 II - 力扣(LeetCode)
题目描述:
给定一个链表的头节点
head
,返回链表开始入环的第一个节点。 如果链表无环,则返回null
。如果链表中有某个节点,可以通过连续跟踪
next
指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。如果pos
是-1
,则在该链表中没有环。注意:pos
不作为参数进行传递,仅仅是为了标识链表的实际情况。不允许修改 链表。
class Solution
{
public:ListNode* detectCycle(ListNode* head) {//创建一个set,存储的是每个节点的的内存地址set<ListNode*> s;ListNode* cur = head;//遍历链表while (cur){//新地址插入setauto ret = s.insert(cur);//重复地址则退出循环if (ret.second == false)return cur;cur = cur->next;}return nullptr;}
};
解题思路:
这段代码的目的是在给定的链表中,找到环的起始节点。如果链表中没有环,则返回 nullptr
(空指针)。
它所采用的核心思想是:遍历链表,并使用一个集合(std::set
)来记录所有已经访问过的节点的内存地址。
在遍历过程中,每访问一个新节点,就尝试将其地址存入集合中。因为集合中不能有重复元素,所以:
- 如果一个节点地址可以成功插入集合,说明这个节点是第一次被访问。
- 如果一个节点地址无法插入集合,说明这个地址之前已经存在于集合中了,这意味着再次访问到了同一个节点,因此也就找到了环。这个无法被插入的节点,就是环的起始节点。
6. map
6.1 map 的介绍
map 和 set 一样都是按照一定次序存储元素的容器,其底层用红黑树实现,和 set 不同的是,map 是 key/value 模型 的容器,在map 中,键值 key 通常用于排序和惟一地标识元素,而值 value中 用于存储与此键值 key 关联的内容,键值 key 和值value的类型可以不同。在 map 内部,key-value 通过成员类型 pair 绑定在一起,也就是文章开始提到的键值对。
需要注意的是:map 中的元素是按照键值 key 进行比较排序的,而与 key 对应的 value 无关,同时,map 中也不允许有重复 key 值的节点;map 也可用于排序、查找和去重,且 map 查找的时间复杂度也为 O(logN),迭代器遍历是走的中序,所以是按key有序遍历的。。
下面是 map 的声明:
template < class Key, // map::key_typeclass T, // map::mapped_typeclass Compare = less<Key>, // map::key_compareclass Alloc = allocator<pair<const Key,T> > // map::allocator_type> class map;
总结:
- Key 就是 map 底层关键字的类型,T 是 map 底层 value 的类型。
- map 默认要求 Key 支持小于比较,如果不支持或者需要的话可以自行实现仿函数传给第二个模板参数。
- map 底层存储数据的内存是从空间配置器申请的,如果需要自己实现内存池,可以传给第三个参数。
- 一般情况下,都不需要传后两个模板参数。
特别注意:map 允许修改节点中 key 对应的 value 的值,但是不允许修改 key,因为这样可能会破坏搜索树的结构。
6.2 map 的使用
6.2.1 构造
// empty (1) 无参默认构造
explicit map (const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());// range (2) 迭代器区间构造
template <class InputIterator>map (InputIterator first, InputIterator last,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());// copy (3) 拷贝构造
map (const map& x);// initializer list (5) initializer 列表构造
map (initializer_list<value_type> il,const key_compare& comp = key_compare(),const allocator_type& alloc = allocator_type());
示例代码:
#include<iostream>
#include<map>
#include<string>
using namespace std;int main()
{// initializer_list构造及迭代遍历 map<string, string> dict = { {"left", "左边"}, {"right", "右边"},{"insert", "插⼊"},{ "string", "字符串" } };//map<string, string>::iterator it = dict.begin();auto it = dict.begin();while (it != dict.end()){//对map中的pair键值对进行读取//cout << (*it).first <<":"<<(*it).second << endl;cout << it->first << ":" << it->second << endl;++it;}cout << endl;return 0;
}
注意:
-
在构造 map 时使用了第五种列表构造的方式,但是这里涉及利用了 pair 匿名对象与隐式类型转换的知识点。
因为这里的 initializer list 要求花括号中以一个一个 pair 类型的对象构成一个花括号列表,但是如果一个个的定义一个 pair 对象再将其写到花括号列表中就太浪费了,又因为 pair 的构造函数允许它直接从一个花括号列表
{元素1, 元素2}
来通过匿名对象构建。当编译器这样做时,它会进一步发现元素1
和元素2
的类型 (const char*
) 与目标pair
成员的类型 (std::string
) 不匹配,于是便触发 string 的隐式类型转换。所以,是pair
的构造机制 和string
的转换机制 共同作用,才实现了这种便捷的构造方式。注意:这里的隐式类型转换,并不是
pair
整体支持某种转换,而是它调用的构造函数允许它的各个参数分别进行隐式类型转换,比如上面的 left 作为 pair 的一个参数由 char 类型隐式类型转换成 string 类型。//原版书写方式 int main() {pair<string, string> kv1 = {"insert", "插入"};pair<string, string> kv2 = {"left", "左边"};pair<string, string> kv3 = {"right", "右边"};pair<string, string> kv4 = {"string", "字符串"};map<string, string> dict = { kv1, kv2, kv3, kv4 }; }
-
在使用迭代器访问 map 中的 pair 类的数据时候也有一些注意点。
因为 pair 类中并没有实现流插入和流提取,所以
cout << (*it).first <<":"<<(*it).second << endl;
这中读取其中数据的方式是错误的。因为 pair 是以结构体的方式定义的,所以可以通过
.
和->
直接访问其中的key
和value
,但是在使用->
访问的时候需要知道,operator ->
这里的返回值是pair 类型的指针,所以如果需要在访问其中的key
或者value
还需要加一个->
。但是一般只需要写一个->
就可以。cout << it.operator->()->first << ":" << it.operator->()->second << endl;cout << it->first << ":" << it->second << endl;
6.2.2 迭代器
// 迭代器是⼀个双向迭代器
iterator -> a bidirectional iterator to const value_type// 正向迭代器
iterator begin();
iterator end();// 反向迭代器
reverse_iterator rbegin();
reverse_iterator rend();
因为 map 支持迭代器也就支持范围 for,但是注意 map 使用范围 for 遍历的时候要使用引用,否则涉及深拷贝的内存浪费问题。
// 范围for遍历
for (const auto& e : dict)
{cout << e.first << ":" << e.second << endl;
}
cout << endl;//结构化绑定 C++17支持
for(const auto& [k, v] : dict)
{cout << k << ":" << v << endl;
}
cout << endl;
6.2.3 修改
// Member types
key_type -> The first template parameter (Key)
mapped_type -> The second template parameter (T)
value_type -> pair<const key_type, mapped_type>// 单个数据插入,如果已存在则插入失败
pair<iterator,bool> insert (const value_type& val);// 列表插入,已经在容器中的值不会插入
void insert (initializer_list<value_type> il);// 迭代器区间插入,已经在容器中存在的值不会插入
template <class InputIterator>
void insert (InputIterator first, InputIterator last);// 删除一个迭代器位置的值
iterator erase (const_iterator position);// 删除k,k存在返回1,不存在返回0
size_type erase (const key_type& k);// 删除一段迭代器区间的值
iterator erase (const_iterator first, const_iterator last);
修改中的重点的仍然是 insert 和 erase,swap 为交换两棵树的根,clear 为释放树中的每一个节点。
insert
和 set 一样,map 的 insert 也支持插入一个值、在某个迭代器位置插入值、插入一段迭代器区间,我们还是学会第一个即可,插入的过程就是二叉搜索树的插入过程。
需要注意的是 insert 的返回值是 pair 类型,pair 中第一个元素代表插入的迭代器位置,第二个元素代表是否插入成功 (插入重复节点会返回 false)
#include<iostream>
#include<map>
using namespace std;int main()
{// initializer_list构造及迭代遍历 map<string, string> dict = { {"left", "左边"}, {"right", "右边"},{"insert", "插⼊"},{ "string", "字符串" } };// insert插⼊pair对象的4种⽅式// 1.先构造对象在插入pair<string, string> kv1("first", "第⼀个");dict.insert(kv1);// 2.匿名对象插入dict.insert(pair<string, string>("second", "第⼆个"));// 3.make_pair函数模版插入dict.insert(make_pair("sort", "排序"));// 4.多参数隐式类型转换+匿名对象插入(最好用)dict.insert({ "auto", "⾃动的" });// "left"已经存在,插⼊失败 dict.insert({ "left", "左边,剩余" });return 0;
}
erase
erase 一样也有三种,常用的是第一种和第二种,删除指定键值的数据和删除指定迭代器位置的数据:
erase 和 set 中完全类似,这里就不做演示
6.2.4 操作
和 set 一样,map 中最重要的还是 find ,有 count 函数是因为 multimap 需要count 函数,这里是为了保持一致性:
// 查找,返回k所在的迭代器,没有找到返回end()
iterator find (const key_type& k);// 查找,返回k的个数
size_type count (const key_type& k) const;// 返回大于等于k位置的迭代器
iterator lower_bound (const key_type& k);// 返回大于k位置的迭代器
const_iterator lower_bound (const key_type& k) const;
find
利用 find 接口,和前面在 map 中插入的数据,可以完成在英文字典中输入英文显示对应中文的一段小代码。
#include<iostream>
#include<map>
using namespace std;int main()
{// initializer_list构造及迭代遍历 map<string, string> dict = { {"left", "左边"}, {"right", "右边"},{"insert", "插⼊"},{ "string", "字符串" } };string str;while (cin >> str){auto ret = dict.find(str);if (ret != dict.end()){cout << "->" << ret->second << endl;}else{cout << "⽆此单词,请重新输⼊" << endl;}}return 0;
}
count 等其它接口与 set 中一致就不做过多演示。
6.2.5 元素访问
6.2.5.1 函数原型
需要重点注意的是,map 重载了 [] 运算符,让其达到可以利用 [] 访问 map 中数据的目的,其函数原型如下:
//mapped_type: pair中第二个参数,即first
//key_type: pair中第一个参数,即second
mapped_type& operator[] (const key_type& k);//本质:相当于传一个键值对中的 key 可以找到与之对应的 value ,并且返回的是 value 的引用,可以进行修改
6.2.5.2 函数定义
mapped_type& operator[] (const key_type& k)
{(*((this->insert(make_pair(k,mapped_type()))).first)).second;
}
可以看到 operator[] 的重载函数的底层是利用 insert 是实现的,所以要想看懂这段代码,需要先深刻理解 insert 。
insert 的底层原理:
文档中对insert返回值的说明
The single element versions (1) return a pair, with its member pair::first set to an iterator pointing to either the newly inserted element or to the element with an equivalent key in the map. The pair::second element in the pair is set to true if a new element was inserted or false if an equivalent key already existed.
insert插入一个pair<key, T>对象
- 如果key已经在map中,插入失败,则返回一个pair<iterator,bool>对象,返回pair对象first是key所在结点的迭代器,second是false
- 如果key不存在map中,插入成功,则返回一个pair<iterator,bool>对象,返回pair对象first是新插入key所在结点的迭代器,second是true
也就是说无论插入成功还是失败,返回 pair<iterator,bool> 对象的 first 都会指向key所在的迭代器
那么也就意味着 insert 插入失败时充当了查找的功能,正是因为这一点, insert 可以用来实现 operator[]
需要注意的是这里有两个 pair ,不要混淆了,一个是 map 底层红黑树节点中存的 pair<key, T> ,另一个是insert返回值 pair<iterator,bool>
5.2.5.3 内部实现
// operator的内部实现
mapped_type& operator[] (const key_type& k)
{pair<iterator, bool> ret = insert({ k, mapped_type() });iterator it = ret.first;return it->second;
}
补充:
- 如果 k 不在 map 中,insert 会插⼊ k 和 mapped_type(value) 默认值,同时 [] 返回结点中存储 mapped_type 值的引⽤,那么可以通过引⽤修改返映射值。所以 [] 具备了插⼊+修改功能
- 如果 k 在 map 中,insert 会插⼊失败,但是 insert 返回 pair 对象的 first 是指向 map 中和这个 key 结点相等的迭代器,返回值同时 [] 返回结点中存储 mapped_type 值的引⽤,所以[]具备了查找+修改的功能
6.2.5.4 代码示例
示例1:
#include<iostream>
#include<map>
#include<string>
using namespace std;int main()
{map<string, string> dict;dict.insert(make_pair("sort", "排序"));// key不存在->插⼊ {"insert", string()} dict["insert"];// 插⼊+修改 dict["left"] = "左边";// 修改 dict["left"] = "左边、剩余";// key存在->查找 cout << dict["left"] << endl;return 0;
}
示例2:
#include<iostream>
#include<map>
#include<string>
using namespace std;int main()
{// 利⽤[]插⼊+修改功能,巧妙实现统计⽔果出现的次数 string arr[] = { "苹果", "西⽠", "苹果", "西⽠", "苹果", "苹果", "西⽠","苹果", "⾹蕉", "苹果", "⾹蕉" };map<string, int> countMap;for (const auto& str : arr){// []先查找⽔果在不在map中 // 1、不在,说明⽔果第⼀次出现,则插⼊{⽔果, 0},同时返回次数的引⽤,++⼀下就变成1次了// 2、在,则返回⽔果对应的次数++ countMap[str]++;}for (const auto& e : countMap){cout << e.first << ":" << e.second << endl;}cout << endl;return 0;
}
7. multimap
和 set 与 multiset 的关系一样,multimap 存在的意义是允许 map 中存在 key 值相同的节点,multimap 与map 的区别和 multiset 与 set 的区别一样。
find 返回中序遍历中遇到的第一个节点的迭代器,count 返回和 key 值相等的节点的个数:
注意:
multimap 中并没有重载 [] 运算符,因为 multimap 中的元素是可以重复的,如果使用 [] 运算符,会导致多个元素的 key 值相同,无法确定具体访问哪一个元素。
8. 有关 map 的面试题
8.1 随机链表的复制
题目链接:138. 随机链表的复制 - 力扣(LeetCode)
题目描述:
给你一个长度为
n
的链表,每个节点包含一个额外增加的随机指针random
,该指针可以指向链表中的任何节点或空节点。构造这个链表的 深拷贝。 深拷贝应该正好由
n
个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的next
指针和random
指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。例如,如果原链表中有
X
和Y
两个节点,其中X.random --> Y
。那么在复制链表中对应的两个节点x
和y
,同样有x.random --> y
。返回复制链表的头节点。
用一个由
n
个节点组成的链表来表示输入/输出中的链表。每个节点用一个[val, random_index]
表示:
val
:一个表示Node.val
的整数。random_index
:随机指针指向的节点索引(范围从0
到n-1
);如果不指向任何节点,则为null
。你的代码 只 接受原链表的头节点
head
作为传入参数。
class Solution
{
public:Node* copyRandomList(Node* head) {// 初始化map<Node*, Node*> nodeMap;Node* copyhead = nullptr, * copytail = nullptr; // 新链表的头指针和尾指针Node* cur = head; // 用于遍历原链表的指针// 开始遍历原链表while (cur){if (copytail == nullptr){copyhead = copytail = new Node(cur->val);}else{copytail->next = new Node(cur->val);copytail = copytail->next;}// 原节点和拷⻉节点map kv存储 // 这是最关键的一步,key是原节点指针,value是新节点指针,将二者存在一个map中建立联系nodeMap[cur] = copytail;cur = cur->next;}// 处理random // 重置指针,准备同时遍历两个链表cur = head; // 指向原链表头Node* copy = copyhead; // 指向新链表头// 开始同时遍历while (cur){if (cur->random == nullptr){// 如果原节点的 random 指针为空,新节点也应为空copy->random = nullptr;}else{// 如果原节点的 random 指针不为空,它指向原链表中的某个节点 (cur->random)// 需要让新节点的 random 指针指向该节点对应的新节点// 这个对应关系正好存储在 map 中copy->random = nodeMap[cur->random];}cur = cur->next;copy = copy->next;}return copyhead;}
};
解题思路:
第一步:复制节点本身以及 next
指针关系(第6~28行)
这个步骤的目标是:
- 遍历原链表。
- 为原链表中的每一个节点创建一个新的对应节点。
- 将这些新节点通过
next
指针连接起来,形成一个基本的链表结构。 - 同时,将“原节点 -> 新节点”的映射关系存储在
map
中,以备后用。
执行完这个循环后,得到了一个新链表 (copyhead
),它的 val
和 next
指针都已经被正确复制。同时,nodeMap
中保存了所有原节点和它们对应的新节点的地址,并建立起对应关系。但是,新链表中所有节点的 random
指针此时还是默认值(nullptr
)。
第二步:处理 random
指针
现在有了新旧节点的映射关系,可以轻松地为新链表设置正确的 random
指针了。
这个步骤的目标是:
- 再次遍历原链表和新链表。
- 对于原链表中的每一个节点,找到它的
random
指针指向的节点。 - 利用
map
,找到这个random
指针目标节点所对应的新节点。 - 将新链表中对应节点的
random
指针指向这个新目标节点。
当这个循环结束后,新链表中所有节点的 random
指针也都设置正确了。
最后一步:返回结果
函数返回新链表的头节点 copyhead
,至此,深拷贝完成。
8.2 前K个高频单词
题目链接:692. 前K个高频单词 - 力扣(LeetCode)
题目描述:
给定一个单词列表
words
和一个整数k
,返回前k
个出现次数最多的单词。返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率, 按字典顺序 排序。
解法一:
用排序找前 k 个单词,因为 map 中已经对 key 单词排序过,也就意味着遍历 map 时,次数相同的单词,字典序小的在前面,字典序大的在后面。 那么将数据放到 vector 中用一个稳定的排序就可以实现上面特殊要求,但是sort底层是快排选基准值时与基准值相同的数据有可能在左有可能在右,是不稳定的,所以要用 stable_sort ,它是稳定的。
class Solution
{
public:struct Compare{//仿函数,这个函数定义了如何比较两个元素x和ybool operator()(const pair<string, int>& x, const pair<string, int>& y) const{return x.second > y.second;}};vector<string> topKFrequent(vector<string>& words, int k) {//频率统计map<string, int> countMap;for (auto& e : words){countMap[e]++;}//将map中的所有key-value对转换成一个vector//并且因为 countMap 本身是按键(单词)的字典序排列的,所以转换后的 v 初始状态也是按单词的字典序排列的。vector<pair<string, int>> v(countMap.begin(), countMap.end());// 仿函数控制降序stable_sort(v.begin(), v.end(), Compare())// 取前k个 vector<string> strV;for (int i = 0; i < k; ++i){strV.push_back(v[i].first);}return strV;}
};
解题思路:
频率统计: 使用 map
数据结构遍历整个单词列表,统计出每个唯一单词的出现次数。
排序: 将统计结果从 map
转移到一个 vector
中,然后使用自定义的排序规则对 vector
进行排序。
结果提取: 从排好序的的 vector
中提取出前 k
个单词作为最终结果。
解法二:
将 map 统计出的次数的数据放到 vector 中排序,或者放到 priority_queue 中来选出前 k 个。利用仿函数强行控制次数相等的,字典序小的在前面。
class Solution
{
public:struct Compare{//仿函数,这个函数定义了如何比较两个元素x和ybool operator()(const pair<string, int>& x, const pair<string, int>& y) const{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]++;}//将map中的所有key-value对转换成一个vector//并且因为 countMap 本身是按键(单词)的字典序排列的,所以转换后的 v 初始状态也是按单词的字典序排列的。vector<pair<string, int>> v(countMap.begin(), countMap.end());// 将map中的<单词,次数>放到priority_queue中,仿函数控制⼤堆,次数相同按照字典序规则排序 priority_queue<pair<string, int>,vector<pair<string, int>>, Compare> p(countMap.begin(), countMap.end());// 取前k个 vector<string> strV;for (int i = 0; i < k; ++i){strV.push_back(p.top().first);p.pop();}return strV;}
};
解题思路:
频率统计: 这一步和上一个解法完全相同,依然是使用 std::map
来统计每个单词的出现频率。
构建优先队列: 将统计好的 map
中的所有 (单词, 频率)
对一次性地全部放入一个优先队列中。这个优先队列会根据我们提供的自定义比较规则,自动在内部进行堆排序,使得“优先级最高”的元素始终位于队列顶部。
结果提取: 从优先队列的顶部依次弹出 k
个元素,这些就是频率最高的前 k
个单词。