⭐️博主: 此生决int-@CSDN博客
速胜派就是最大的投降派!!!
🔥热门专栏🔥
深入理解 C++ 系列 | 算法系列
快速复习系列 | Java 速通系列
文章目录
-
- 上期回顾
- map和set的使用
-
- 1. 序列式容器和关联式容器⭐️
- 2. set系列的使用
-
- 2.1 set和multiset参考文档
- 2.2 set类的介绍
- 2.3 set的构造和迭代器⭐️
-
- 1,构造
- 2,迭代器
- 2.4 set的增删查⭐️⭐️⭐️
-
- 1,插入insert
- 2,删除eraser
- 3,clear清空所有元素
- 4,查找find
- 5,count统计元素个数
- 6,范围查找——lower_bound和upper_bound
- 运用————找到这样一个区间:[30, 60]
- 2.7 multiset和set的差异
- 3,set相关的简单算法题
-
- 两个数组的交集⭐⭐
-
- 题目链接
- 题目描述
- 解题思路
- 解题代码
- 解题思路2
- 解题代码
- 环形链表 II⭐⭐⭐
-
- 题目链接
- 题目描述
- 解题思路
- 解题代码
- 大神解题代码
- 3. map系列的使用
-
- 3.1 map和multimap参考文档
- 3.2 map类的介绍
- 3.3 pair类型介绍(以后常用!!)⭐️⭐️
- 3.4 map的构造
- 3.5 map的增删查⭐️⭐️
-
- 1,插入insert
- 2,删除erase
- 3,clear清空所有元素
- 4,查找find
- 5,count统计元素个数
- 6,operator[]访问和修改元素⭐️⭐️⭐️
- 3.6 map的数据修改
- operator[]内部实现详解:
- 3.9 multimap和map的差异
- map相关算法题⭐️⭐️⭐️⭐️
-
- 1,复制带随机指针的链表⭐⭐⭐⭐
-
- 题目链接
- 题目描述
- 解题思路
- 我的解题代码
- 优化版,思路一样
- 大神解题代码(深拷贝,直接原地复制数组)
- 2,前 K 个高频单词⭐⭐
-
- 题目链接
- 题目描述
- 解题思路
- 解题代码
- 大神解题代码
- 下期预告
- AVL树
- 结语
上期回顾
上一篇我们主要学习了 二叉搜索树,了解了二叉搜索树的相关特性,手动模拟实现了key版和key/value版本的普通的二叉搜索树,那么今天,我们就来用一用STL里面封装好了并且优化了的二叉搜索树——set和map系列!让我们开始今天的学习之旅吧!
map和set的使用
1. 序列式容器和关联式容器⭐️
简单来说: 序列式容器:逻辑为线性结构,两个位置的值之间没有关系,交换一下,它依旧是序列式容器。,例如,string,vector,list等等 关联式容器:逻辑非线性结构,两个位置的值之间有关系,交换一下,它的存储结构就被破坏了。,例如,二叉搜索树(map/set)和unordered_map/unordered_set 系列。

2. set系列的使用
2.1 set和multiset参考文档
set和multiset参考文档
2.2 set类的介绍
总结: 1,set 2,set<T,greater>, 第一个参数为键值,,默认小根,如果要大根,第二个参数传仿函数,set<T,Greater> 3,增删查效率是 O(logN),不支持改!!,
template < class T, // set::key_type/value_type
class Compare = less<T>, // set::key_compare/value_compare
class Alloc = allocator<T> // set::allocator_type
> class set;
2.3 set的构造和迭代器⭐️
1,构造
set<int> s1;//默认构造
set<int> s2(s1);//拷贝构造
set<int> s3(s1.begin(),s1.end());//迭代器区间构造
set<int> s4={1,5,3,7};//初始化列表构造
2,迭代器
1,set是双向迭代器,所以有begin()/rbegin(),end()/rend(); 2,set的迭代器不支持修改数据 3,范围for遍历采用中序遍历,遍历是有序的
2.4 set的增删查⭐️⭐️⭐️
1,插入insert
示例代码如下:
//set不允许插入重复元素
//直接插入value
s.insert(10);
//迭代器插入
set<int> s1={1,2,3,5,6};
s.insert(s1.begin(),s1.end());
// 插入一段 initializer_list 列表值,已经存在的值插入失败
s.insert({ 2,8,3,9 });
2,删除eraser
1,erase按照key删除
s.erase(10);
2,erase按照迭代器删除
auto it=s.begin();
s.erase(it);
3,erase按照迭代器区间删除
s.erase(s.begin(),s.end());
注意:删除后迭代器失效!!!
3,clear清空所有元素
4,查找find
1,find查找元素
auto it=s.find(key);
返回值 1,找到:返回元素迭代器 2,未找到:返回end() 这里注意,库里面也用一个find,但是效率太慢了(O(N)),优先使用set的find函数接口
// 算法库的查找 O(N)
auto pos1 = find(s.begin(), s.end(), x);
// set 自身实现的查找 O(logN)
auto pos2 = s.find(x);
//find和eraser的配合
// 直接查找再利用迭代器删除 x
cin >> x;
auto pos = s.find(x);
if (pos != s.end())
{
s.erase(pos);
}
else
{
cout << x << "不存在!" << endl;
}
相似的:
优先使用库里的情况:
总之,容器自己实现了肯定优先使用容器的
5,count统计元素个数
s.count(key);
因为set不允许重复:所以只能返回0(不存在)和1(存在)
主要运用:查看是否存在某个key⭐️⭐️⭐️
// 利用 count 间接实现快速查找
cin >> x;
if (s.count(x))
{
cout << x << "在!" << endl;
}
else
{
cout << x << "不存在!" << endl;
}
6,范围查找——lower_bound和upper_bound
1,lower_bound
auto it=s.lower_bound(key);//返回第一个大于等于key的元素
auto it=s.upper_bound(key);//返回第一个大于key的元素
运用————找到这样一个区间:[30, 60]
std::set<int> myset;
for (int i = 1; i < 10; i++)
myset.insert(i * 10); // 10 20 30 40 50 60 70 80 90
// 实现查找到的 [itlow, itup) 包含 [30, 60] 区间
// 返回 >= 30的第一个迭代器log(N);
auto itlow = myset.lower_bound(30);
// 返回 > 60的第一个迭代器log(N)
auto itup = myset.upper_bound(60);
// 删除这段区间的值
myset.erase(itlow, itup);
2.7 multiset和set的差异
multiset: find,找中序遍历的第一个key eraser 为key的所有全部删掉 cont 有几个key返回几
3,set相关的简单算法题
两个数组的交集⭐⭐
题目链接
两个数组的交集
题目描述
给定两个数组 nums1 和 nums2,返回它们的交集。结果中的每个元素必须是唯一的,返回结果可以按照任意顺序。 
解题思路
利用 set 的去重和查找特性。先将第一个数组中的元素存入 set,然后遍历第二个数组,如果元素存在于第一个 set 中,则加入结果 set,最后转换成 vector 返回。
解题代码
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
// 存储 nums1 中的元素,自动去重
set<int> s1;
// 存储最终交集,保证结果唯一
set<int> ret;
// 将 nums1 中所有元素加入集合
for (auto it : nums1)
s1.insert(it);
// 判断 nums2 中的元素是否存在于 nums1 集合中
for (auto it : nums2)
{
if (s1.count(it))
ret.insert(it);
}
vector<int> rret;
// set 转换为 vector
for (auto it : ret)
rret.push_back(it);
return rret;
}
};
解题思路2
利用 set 自动去重并排序的特点。
先分别将两个数组转换成 set,然后使用两个迭代器同时遍历两个集合:
- 如果两个元素相等,说明找到交集,加入结果。
- 如果当前 nums1 的元素较小,则移动 nums1 的迭代器。
- 否则移动 nums2 的迭代器。
类似归并排序中的双指针思想。
解题代码
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
set<int> s1;
set<int> s2;
vector<int> ret;
// 保存 nums1 中的元素,自动去重并排序
for (int x : nums1)
{
s1.insert(x);
}
// 保存 nums2 中的元素,自动去重并排序
for (int x : nums2)
{
s2.insert(x);
}
// 两个 set 同时遍历
auto cur1 = s1.begin();
auto cur2 = s2.begin();
while (cur1 != s1.end() && cur2 != s2.end())
{
if (*cur1 == *cur2)
{
// 找到公共元素
ret.push_back(*cur1);
cur1++;
cur2++;
}
else
{
// 小的元素不可能再匹配,移动较小的一方
if ((*cur1) < (*cur2))
cur1++;
else
cur2++;
}
}
return ret;
}
};
环形链表 II⭐⭐⭐
题目链接
环形链表 II
题目描述
给定一个链表的头节点 head,判断链表是否存在环。如果存在环,返回环的入口节点;如果不存在环,返回 nullptr。 
解题思路
使用 set 记录已经访问过的节点地址。
遍历链表时,如果当前节点已经存在于 set 中,说明之前访问过该节点,即链表形成环,当前节点就是环的入口。
如果遍历结束仍未重复,则说明不存在环。
解题代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(0), next(NULL) {}
* };
*/
class Solution {
typedef struct ListNode listnode;
public:
typedef struct ListNode ListNode;
ListNode *detectCycle(ListNode *head) {
// 保存已经访问过的节点地址
set<ListNode*> s1;
auto it = head;
while (it)
{
// 如果节点已经访问过,说明进入环
if (s1.count(it))
return it;
// 记录当前节点
s1.insert(it);
// 继续向后遍历
it = it->next;
}
// 遍历结束,没有环
return it;
}
};
没懂?看看大神的解题代码!!
大神解题代码
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode(int x) : val(0), next(NULL) {}
* };
*/
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if (head == nullptr)
return nullptr;
ListNode* slow = head;
ListNode* fast = head;
// 快慢指针判断是否有环
while (fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast)
{
// 找环入口
ListNode* cur = head;
while (cur != slow)
{
cur = cur->next;
slow = slow->next;
}
return cur;
}
}
return nullptr;
}
};
3. map系列的使用
3.1 map和multimap参考文档
map和multimap参考文档
3.2 map类的介绍
与set几乎一样,只是第一个参数变成了pair类型 map<pair<T1,T2>> mp; mp<pair<T1,T2>,greater>mp; 第一个参数是一个pair类型,第二个是仿函数
3.3 pair类型介绍(以后常用!!)⭐️⭐️
pair本质上就是:用一个结构体把两个值封装起来了,map里面的pair如下:
typedef pair<const Key, T> value_type;//注意,mp里面的第一个参数是const修饰的,不能修改哦,普通的pair是可以修改的
普通pair的底层实现
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)
{}
template<class U, class V>
pair(const pair<U, V>& pr)
: first(pr.first), second(pr.second)
{}
};
template <class T1, class T2>
inline pair<T1, T2> make_pair(T1 x, T2 y)
{
return (pair<T1, T2>(x, y));
}
这里库提供了一个make_pair函数,
pair<string string> p=make_pair("ac","bada");
//同时,C++11支持隐式类型转换,比如,我们后面写insert的时候就可以:
// C++11
dict.insert({ "auto", "自动的" });
3.4 map的构造
map<string, string> dict;//默认构造
pair<string, string> kv1("first", "第一个");
map<string ,string> m2={kv1};//使用pair插入初始化
//map<string string> m1(kv1);没有这种构造哦
map<string, string> dict = { {"left", "左边"}, {"right", "右边"}, {"insert", "插入"},{ "string", "字符串" } };//使用初始化列表构造
map<string,string> dict2(dict1.begin(),dict1.end());//迭代器构造
map<string,string> dict2(m1);//拷贝构造
map的迭代器等与set一致 但map迭代器访问与set不同:因为it是pair类型,pair类型没有重载operator<<,要用->first来访问
auto it=m.find(1);
cout<<it->first<<endl;//key
cout<<it->second<<endl;//value
3.5 map的增删查⭐️⭐️
1,插入insert
map<int,string> m;
//插入pair
m.insert(pair<int,string>(1,"张三"));
//C++11支持隐式类型转换,所以支持这种写法
m.insert({2,"李四"});
注意; 1,map不允许插入重复key 2,key会自动排序 3,value可以重复
2,插入一段迭代器区间
map<int,string> m1=
{
{1,"a"},
{2,"b"},
{3,"c"}
};
map<int,string> m2;
m2.insert(m1.begin(),m1.end());
3,插入initializer_list列表值
m.insert({
{4,"d"},
{5,"e"}
});
注意: 如果key已经存在,插入失败,不会覆盖原来的value
2,删除erase
1,erase按照key删除
m.erase(1);
0 //删除失败,key不存在
1 //删除成功
2,erase按照迭代器删除
auto it=m.begin();
m.erase(it);
3,erase按照迭代器区间删除
m.erase(m.begin(),m.end());
注意: 删除后迭代器失效!!!
3,clear清空所有元素
4,查找find
1,find查找元素
auto it=m.find(key);
1,找到:返回键值对的迭代器 2,未找到:返回end()
5,count统计元素个数
m.count(key);
因为map不允许重复key:所以只能返回: 1,存在:1 2,不存在:0
主要运用:查看某个key是否存在
int x;
cin>>x;
if(m.count(x))
{
cout<<x<<"存在!"<<endl;
}
else
{
cout<<x<<"不存在!"<<endl;
}
注意: map也可以使用count实现快速查找是否存在,但是推荐find,因为find可以直接获得对应的value。
6,operator[]访问和修改元素⭐️⭐️⭐️
operator[]访问和修改元素是map最常用接口之一
例如:
map<int,string> m;
m[1]="张三";
cout<<m[1];
注意: 1,根据key访问value 2,不存在时会插入新的键值对 3,如果只是判断key是否存在,不推荐使用:operator[],因为会导致插入。推荐:find
3.6 map的数据修改
前面我提到 map 支持修改 pair的第二个 数据,不支持修改 key 数据,修改关键字数据会破坏底层搜索树的结构。
两种修改方法: 1,通过迭代器修改 2,operator[]修改,注意,如果不存在这个key,它会直接插入!!
operator[]内部实现详解:
首先我们要知道,operator[]既可以增加key,也可以查找,所以,operator[]内部是用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> 对象
也就是说无论插入成功还是失败,返回 pair<iterator, bool> 对象的 first 都会指向 key 所在的迭代器。那么也就意味着 insert 插入失败时充当了查找的功能,正是因为这一点,insert 可以用来实现 operator[]。 需要注意的是这里有两个 pair,不要混淆了:
- 一个是 map 底层红黑树节点中存的 pair<key, T>,
- 另一个是 insert 返回值 pair<iterator, bool>。
pair<iterator,bool> insert (const value_type& val);
mapped_type& operator[] (const key_type& k);
// operator[] 的内部实现
mapped_type& operator[] (const key_type& k)
{
// 1、如果 k 不在 map 中,insert 会插入 k 和 mapped_type 默认值,
// 同时 [] 返回结点中存储 mapped_type 值的引用,
// 那么我们可以通过引用修改映射值。所以 [] 具备了插入+修改功能。
// 2、如果 k 在 map 中,insert 会插入失败,但是 insert 返回 pair 对象的 first
// 是指向 key 结点的迭代器,[] 同时返回结点中存储 mapped_type 值的引用,
// 所以 [] 具备了查找+修改的功能。
pair<iterator, bool> ret = insert({ k, mapped_type() });
iterator it = ret.first;
return it->second;
}
3.9 multimap和map的差异
multimap 和 map 的使用基本完全类似,主要区别点在于 multimap 支持关键值 key 冗余,那么 ins ert/find/count/erase 都围绕着支持关键值 key 冗余有所差异,这里跟 set 和 multiset 完全一样。比如 find 时,有多个 key,返回中序第一个。其次就是 multimap 不支持 [],因为支持 key 冗余,[] 就只能支持插入了,不能支持修改。
map相关算法题⭐️⭐️⭐️⭐️
1,复制带随机指针的链表⭐⭐⭐⭐
题目链接
复制带随机指针的链表
题目描述
给你一个长度为 n 的链表,每个节点包含:
- val:节点值
- next:指向下一个节点
- random:可以指向链表中的任意节点或者 null
要求返回链表的深拷贝。 
解题思路
使用 map 建立 原链表节点 -> 新链表节点 的映射。
分两步:
我的解题代码
/*
// Definition for a Node.
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = NULL;
random = NULL;
}
};
*/
class Solution {
public:
Node* copyRandomList(Node* head) {
// 保存 原节点 -> 新节点 的映射
map<Node*, Node*> m;
Node* rhead = head;
// 创建虚拟头节点,方便连接新链表
Node* rethead = new Node(–1);
Node* ret = rethead;
// 第一次遍历:复制所有节点
while (head)
{
Node* tmp = new Node(head->val);
rethead->next = tmp;
rethead = tmp;
// 建立映射关系
m[head] = rethead;
head = head->next;
}
// 删除虚拟头节点,返回真正的头节点
Node* tmp = ret;
ret = ret->next;
delete tmp;
// 保存原链表头,用于第二次遍历
while (rhead)
{
// 根据映射关系设置 random 指针
m[rhead]->random = m[rhead->random];
rhead = rhead->next;
}
return ret;
}
};
优化版,思路一样
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 存储
nodeMap[cur] = copytail;
cur = cur->next;
}
// 处理 random
cur = head;
Node* copy = copyhead;
while (cur)
{
if (cur->random == nullptr)
{
copy->random = nullptr;
}
else
{
copy->random = nodeMap[cur->random];
}
cur = cur->next;
copy = copy->next;
}
return copyhead;
}
};
没懂?看看大神的解题代码!!
大神解题代码(深拷贝,直接原地复制数组)
class Solution {
public:
Node* copyRandomList(Node* head) {
if (head == nullptr)
return nullptr;
// 第一步:复制节点,并插入到原节点后面
Node* cur = head;
while (cur)
{
Node* copy = new Node(cur->val);
copy->next = cur->next;
cur->next = copy;
cur = copy->next;
}
// 第二步:处理 random 指针
cur = head;
while (cur)
{
if (cur->random)
cur->next->random = cur->random->next;
cur = cur->next->next;
}
// 第三步:拆分链表
Node* ret = head->next;
cur = head;
while (cur)
{
Node* copy = cur->next;
cur->next = copy->next;
if (copy->next)
copy->next = copy->next->next;
cur = cur->next;
}
return ret;
}
};
2,前 K 个高频单词⭐⭐
题目链接
前 K 个高频单词
题目描述
给定一个字符串数组 words 和一个整数 k,返回出现频率最高的 k 个单词。
答案需要按照:

解题思路
先使用哈希表统计每个单词出现的次数。
然后利用优先级队列(堆)进行排序,通过自定义比较规则:
- 出现次数多的优先级更高;
- 出现次数相同时,字典序小的优先级更高。
最后不断取出堆顶的 k 个单词即可。
解题代码
class Solution {
// 自定义堆的比较规则
struct cmp {
bool operator()(pair<int, string> a, pair<int, string> b)
{
// 频率不同,频率高的优先级更高
if (a.first != b.first)
return a.first < b.first;
// 频率相同,字典序小的优先级更高
// priority_queue 默认大堆,所以这里反向比较
return b.second < a.second;
}
};
public:
vector<string> topKFrequent(vector<string>& words, int k) {
priority_queue<pair<int,string>,
vector<pair<int,string>>,
cmp> pq;
// 统计每个单词出现次数
unordered_map<string,int> m;
for (auto it : words)
m[it]++;
// 将所有单词加入堆
for (auto it : m)
{
pq.push({it.second, it.first});
}
vector<string> ret;
// 取出前 k 个高频单词
while (k—)
{
string tmp = pq.top().second;
pq.pop();
ret.push_back(tmp);
}
return ret;
}
};
没懂?看看大神的解题代码!!
大神解题代码
class Solution {
public:
vector<string> topKFrequent(vector<string>& words, int k) {
unordered_map<string,int> cnt;
// 统计出现次数
for (auto& word : words)
cnt[word]++;
// 小根堆,只保留 k 个元素
auto cmp = [](pair<string,int>& a, pair<string,int>& b)
{
// 频率低的优先弹出
if (a.second != b.second)
return a.second > b.second;
// 频率相同时,字典序大的优先弹出
return a.first < b.first;
};
priority_queue<pair<string,int>,
vector<pair<string,int>>,
decltype(cmp)> pq(cmp);
for (auto& [word, count] : cnt)
{
pq.push({word, count});
// 超过 k 个,删除当前最差元素
if (pq.size() > k)
pq.pop();
}
vector<string> ret;
while (!pq.empty())
{
ret.push_back(pq.top().first);
pq.pop();
}
// 小根堆取出顺序相反,需要反转
reverse(ret.begin(), ret.end());
return ret;
}
};
下期预告
AVL树
结语
本文到此结束,感谢大家的阅读!如果觉得本文对你有所帮助,欢迎点赞、收藏、关注,也欢迎在评论区一起交流讨论。 也欢迎订阅我的 深入理解 C++系列:从语法入门到底层原理,系统掌握现代 C++ 算法系列:从入门到精通,蓝桥杯、ACM、LeetCode 与面试算法全路线 快速复习系列:知识梳理、查漏补缺,考前冲刺必备 Java 速通系列:已学 C 语言,快速上手 Java,轻松备战期末考试
愿每一次敲下键盘,都比昨天更进一步!
愿每一行代码落下,都让未来多一种可能!




