
目录
[1. 序列式容器 和 关联式容器](#1. 序列式容器 和 关联式容器)
[1.1 序列式容器](#1.1 序列式容器)
[1.2 关联式容器](#1.2 关联式容器)
[2 set 系列使用(头文件 )](#2 set 系列使用(头文件 ))
[2.1 set 模板声明](#2.1 set 模板声明)
[2.2 set 常用构造](#2.2 set 常用构造)
[2.3 迭代器](#2.3 迭代器)
[2.4 set 核心成员接口](#2.4 set 核心成员接口)
[2.5 set 基础代码示例](#2.5 set 基础代码示例)
[2.6 find、erase、lower_bound /upper_bound 示例](#2.6 find、erase、lower_bound /upper_bound 示例)
[2.7 multiset(允许重复元素)](#2.7 multiset(允许重复元素))
[2.8 LeetCode 习题](#2.8 LeetCode 习题)
[习题 1:349. 两个数组的交集](#习题 1:349. 两个数组的交集)
[习题 2:142. 环形链表 II,检测环入口](#习题 2:142. 环形链表 II,检测环入口)
[3 map 系列使用(头文件)](#3 map 系列使用(头文件))
[3.1 map 模板](#3.1 map 模板)
[3.2 pair 键值对](#3.2 pair 键值对)
[3.3 map 构造、迭代器](#3.3 map 构造、迭代器)
[3.4 map 核心接口](#3.4 map 核心接口)
[⭐重中之重 operator \[\]](#⭐重中之重 operator [])
[3.5 map 完整示例](#3.5 map 完整示例)
[3.6 multimap](#3.6 multimap)
[3.7 LeetCode 习题](#3.7 LeetCode 习题)
[习题 1:138 复制随机链表](#习题 1:138 复制随机链表)
[习题 2:692. 前 K 个高频单词](#习题 2:692. 前 K 个高频单词)
本节学习目标
- 分清序列式容器 与关联式容器,了解底层红黑树
- 熟练掌握
set/multiset接口:构造、迭代器、insert、find、erase、lower_bound、upper_bound,分清 set 去重、multiset 允许重复 - 掌握
pair键值对结构,理解 map 存储的pair<const Key,T> - 熟练掌握
map/multimap,吃透operator[]底层逻辑(插入、查找、修改三合一) - 区分 map 和 multimap 差异,multimap 不支持
operator[] - 看懂力扣实战习题:两个数组交集、环形链表检测、复制随机链表、前 K 个高频单词
1. 序列式容器 和 关联式容器
1.1 序列式容器
代表:string、vector、list、deque、array、forward_list
- 逻辑是线性序列;元素之间没有强制大小关系。
- 元素依靠存储位置保存、访问;交换两个元素,容器逻辑不会被破坏。
1.2 关联式容器
代表:set / multiset、map / multimap(底层红黑树,平衡二叉搜索树 );还有unordered_set/unordered_map哈希版本。
- 逻辑是非线性树形结构;元素之间依靠关键字 key 维持大小关系;随意交换元素会直接破坏容器结构。
- 元素依靠关键字 key保存和访问;增删查时间复杂度 \(O(logN)\)。
对应前面二叉搜索树知识:
set:对应 key 搜索场景,只存 key,自动排序 + 默认去重map:对应 key‑value 搜索场景,存键值对 pair,key 唯一
2 set 系列使用(头文件 <set>)
2.1 set 模板声明
cpp
template < class T,
class Compare = less<T>, //比较仿函数,默认less<T>升序
class Alloc = allocator<T> >
class set;
- T:存放元素类型;
- Compare:仿函数,默认
less<T>,从小到大升序;传入greater<T>实现降序; - Alloc:空间配置器,内存分配,一般不用管。
- 底层红黑树;迭代器遍历是中序遍历,输出有序;
- set 迭代器是双向迭代器,迭代器指向的内容是 const,不能修改元素!修改 key 会破坏红黑树结构。
2.2 set 常用构造
- 无参构造:
set<int> s; - 迭代器区间构造:
set<int> s(v.begin(),v.end());把容器区间元素传入构造,自动排序去重 - 拷贝构造:
set<int> s2(s1); - 初始化列表构造:
set<int> s{4,2,7,2,8};
2.3 迭代器
begin():返回第一个元素迭代器(最小)end():末尾后一位迭代器,代表不存在元素,不能解引用rbegin()/rend():反向迭代器,逆序遍历
set 支持范围 for,底层等价于正向迭代器遍历。 ❗
*it = xxx;编译报错,set 元素不允许修改。
2.4 set 核心成员接口
表格
| 接口 | 功能说明 |
|---|---|
pair<iterator,bool> insert(val) |
插入元素;key 存在则插入失败;返回 pair:first 迭代器,second 插入是否成功 true/false |
insert(init_list) |
初始化列表批量插入,重复值忽略 |
insert(first,last) |
迭代器区间批量插入,重复值忽略 |
iterator find(val) |
查找 val,找到返回对应迭代器;找不到返回end(),\(O(logN)\) |
size_type count(val) |
统计 val 的个数;set 要么 0 要么 1;multiset 返回真实出现次数 |
iterator erase(pos) |
删除迭代器位置元素,返回下一个元素迭代器 |
size_type erase(val) |
按值删除;set 存在就删返回 1,不存在返回 0 |
iterator erase(first,last) |
删除 [first,last) 区间元素 |
lower_bound(val) |
返回大于等于 val的第一个元素迭代器 |
upper_bound(val) |
返回大于 val的第一个元素迭代器 |
⚠️注意:
std::find是算法库函数,遍历查找\(O(N)\);优先使用容器自带find(),红黑树查找\(O(logN)\)。
2.5 set 基础代码示例
cpp
#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); //重复,插入失败
auto it = s.begin();
while(it != s.end())
{
//*it = 10; //编译报错,不能修改set元素
cout << *it << " ";
++it;
}
cout << endl;
//初始化列表批量插入,重复自动丢弃
s.insert({2,8,3,9});
for(auto e : s)
{
cout << e << " ";
}
cout << endl;
set<string> strset = {"sort", "insert", "add"};
for(auto& e : strset)
{
cout << e << " ";
}
return 0;
}
2.6 find、erase、lower_bound /upper_bound 示例
cpp
#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;
int x;
cin >> x;
size_type num = s.erase(x);
if(num == 0)
{
cout << x << "不存在\n";
}
//find查找删除
cin >> x;
auto pos = s.find(x);
if(pos != s.end())
{
s.erase(pos);
}
else
{
cout << x << "不存在\n";
}
//count间接判断元素是否存在
cin >> x;
if(s.count(x))
cout << x << "在集合中\n";
else
cout << x << "不存在\n";
// lower_bound upper_bound
set<int> myset;
for(int i = 1; i < 10; i++)
myset.insert(i*10); //10,20,30...90
//取区间 [30,60]
auto itlow = myset.lower_bound(30); //>=30
auto itup = myset.upper_bound(60); //>60
myset.erase(itlow, itup); //删除30、40、50、60
for(auto e : myset) cout << e << " ";
return 0;
}
2.7 multiset(允许重复元素)
multiset接口几乎和 set 完全一样,唯一区别:允许插入重复值,不去重。 重点差异:
insert:重复值可以多次插入成功;find(x):返回中序遍历第一个等于 x的迭代器;count(x):返回 x 实际出现总个数;erase(x):传入值,会删除容器中全部等于 x 的元素!;如果只想删一个,先用 find 拿到迭代器,erase (迭代器)。
multiset 示例:
cpp
#include<iostream>
#include<set>
using namespace std;
int main()
{
multiset<int> s = {4,2,7,2,4,8,4,5,4,9};
for(auto e : s) cout << e << " ";
cout << endl;
int x = 4;
auto pos = s.find(x);
while(pos != s.end() && *pos == x)
{
cout << *pos << " ";
++pos;
}
cout << "\ncount=" << s.count(x) << endl;
//s.erase(x); //⚠️删除全部4!!
//只删除一个4,传迭代器
auto delPos = s.find(x);
if(delPos != s.end())
s.erase(delPos);
for(auto e : s) cout << e << " ";
return 0;
}
2.8 LeetCode 习题
习题 1:349. 两个数组的交集349. 两个数组的交集 - 力扣(LeetCode)
思路:两个 set,有序,双指针遍历求交集。
cpp
#include<vector>
#include<set>
using namespace std;
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;
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;
}
};
习题 2:142. 环形链表 II,检测环入口142. 环形链表 II - 力扣(LeetCode)
set 存储链表结点指针;遍历,如果 insert 返回 false 说明结点已经出现过,就是环入口。
cpp
struct ListNode {
int val;
ListNode *next;
};
#include<set>
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;
}
};
3 map 系列使用(头文件<map>)
3.1 map 模板
cpp
template <class Key, class T,
class Compare = less<Key>,
class Alloc = allocator<pair<const Key,T>>>
class map;
- Key:关键字 key;T:映射的 value;
- 底层红黑树,存储
pair<const Key, T>;key 是 const,不允许修改!value 可以修改; - key 唯一,自动按 key 升序排序;
- 迭代器双向迭代器。
3.2 pair 键值对
map 结点存储 pair,first存 key,second存 value。
cpp
template<class T1,class T2>
struct pair
{
T1 first;
T2 second;
pair(){}
pair(const T1& a, const T2& b):first(a),second(b){}
};
//辅助函数,构造pair
template<class T1,class T2>
pair<T1,T2> make_pair(T1 x,T2 y)
{
return pair<T1,T2>(x,y);
}
3.3 map 构造、迭代器
- 无参构造、迭代器区间构造、拷贝构造、初始化列表构造;
begin() end() rbegin() rend()双向迭代器;- 遍历:
it->first取 key,it->second取 value;it->first 不能修改,it->second 可以修改。
3.4 map 核心接口
find、count、erase、lower_bound、upper_bound 接口和 set 用法几乎一致,只是操作对象是 key。
insert返回:pair<iterator,bool>
- 插入成功:first 是新元素迭代器,second=true;
- key 已经存在插入失败:first 指向原有 key 结点迭代器,second=false。
⭐重中之重 operator \[\]
cpp
mapped_type& operator[] (const key_type& k);
operator \[\] 伪代码内部逻辑:
cpp
mapped_type& operator[] (const key_type& k)
{
//key不存在:插入k,value为默认构造值;key存在,insert失败拿到原有迭代器
auto ret = insert({k,mapped_type()});
iterator it = ret.first;
return it->second; //返回value的引用
}
`operator \[\] 三合一功能:
- key 不存在:插入新键值对(value 默认值),返回 value 引用;可以用来插入、赋值;
- key 存在:不插入,直接返回已有 value 引用;可以读取、修改 value;
⚠️注意:只做查找不要直接用
[]!key 不存在会偷偷插入一条数据,污染 map;单纯查找优先用find()。
3.5 map 完整示例
cpp
#include<iostream>
#include<map>
#include<string>
using namespace std;
int main()
{
//初始化列表构造
map<string,string> dict = {{"left","左边"},{"right","右边"},{"insert","插入"}};
auto it = dict.begin();
while(it != dict.end())
{
cout << it->first << ":" << it->second << endl;
++it;
}
//四种insert插入pair
pair<string,string> kv1("first","第一个");
dict.insert(kv1);
dict.insert(pair<string,string>("second","第二个"));
dict.insert(make_pair("sort","排序"));
dict.insert({"auto","自动的"});
dict.insert({"left","左边剩余"}); //key已经存在,插入失败!
//范围for遍历
for(const auto& e : dict)
{
cout << e.first << "->" << e.second << endl;
}
//find查找
string str;
while(cin >> str)
{
auto ret = dict.find(str);
if(ret != dict.end())
{
cout << ret->second << endl;
}
else
{
cout << "没有该单词\n";
}
}
return 0;
}
示例:单词词频统计,两种写法
写法 1:使用 find 判断
cpp
int main()
{
string arr[] = {"苹果","西瓜","苹果","西瓜","苹果","香蕉","苹果","香蕉"};
map<string,int> countMap;
for(const auto& str : arr)
{
auto ret = countMap.find(str);
if(ret == countMap.end())
{
countMap.insert({str,1});
}
else
{
ret->second++;
}
}
for(auto& e : countMap)
{
cout << e.first << " : " << e.second << endl;
}
return 0;
}
写法 2:利用operator[]简洁统计(业务最常用)
cpp
int main()
{
string arr[] = {"苹果","西瓜","苹果","西瓜","苹果","香蕉","苹果","香蕉"};
map<string,int> countMap;
for(const auto& str : arr)
{
countMap[str]++;
}
for(auto& e : countMap)
{
cout << e.first << " : " << e.second << endl;
}
return 0;
}
operator \[\] 多种行为演示
cpp
int main()
{
map<string,string> dict;
dict.insert(make_pair("sort","排序"));
dict["insert"]; //key不存在,插入insert,value为空字符串
dict["left"] = "左边"; //插入+赋值
dict["left"] = "左边、剩余"; //修改已存在key的value
cout << dict["left"] << endl; //读取value
return 0;
}
3.6 multimap
multimap允许 key 重复;
- 接口和 map 基本一致;
- ❗multimap 不支持 operator \[\],因为 key 可以重复,不知道该取哪一个 key 对应的 value;
find(key)返回中序第一个匹配 key 的迭代器;erase(key)删除全部等于 key 的键值对;只想删一个要传迭代器。
3.7 LeetCode 习题
习题 1:138 复制随机链表138. 随机链表的复制 - 力扣(LeetCode)
利用 map 保存原结点→拷贝结点映射,快速处理 random 指针。
cpp
class Node {
public:
int val;
Node* next;
Node* random;
Node(int _val) {
val = _val;
next = nullptr;
random = nullptr;
}
};
#include<map>
class Solution {
public:
Node* copyRandomList(Node* head) {
map<Node*,Node*> nodeMap;
Node* copyhead = nullptr;
Node* copytail = nullptr;
Node* cur = head;
//第一步:拷贝val,建立原结点和拷贝结点映射
while(cur != nullptr)
{
if(copytail == nullptr)
{
copyhead = copytail = new Node(cur->val);
}
else
{
copytail->next = new Node(cur->val);
copytail = copytail->next;
}
nodeMap[cur] = copytail;
cur = cur->next;
}
//第二步处理random指针
cur = head;
Node* copy = copyhead;
while(cur != nullptr)
{
if(cur->random == nullptr)
{
copy->random = nullptr;
}
else
{
copy->random = nodeMap[cur->random];
}
cur = cur->next;
copy = copy->next;
}
return copyhead;
}
};
习题 2:692. 前 K 个高频单词692. 前K个高频单词 - 力扣(LeetCode)
map 统计单词频次;再排序,要求:频次高优先;频次相同字典序小优先。
cpp
#include<vector>
#include<map>
#include<algorithm>
#include<string>
using namespace std;
struct Compare
{
bool 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);
}
};
class Solution {
public:
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());
vector<string> res;
for(int i = 0; i < k; i++)
{
res.push_back(v[i].first);
}
return res;
}
};
📔本篇核心考点总结
- 序列式容器 :依靠位置存储;关联式容器 (set/map 底层红黑树) 依靠 key 关键字存储,O (logN)。
set/multiset:存 key;set 去重;multiset 允许重复;迭代器元素 const,不能修改元素;find()容器内查找优于算法库 std::find。lower_bound >=val;upper_bound >val;erase 传值 multiset 会删除全部匹配元素,只想删一个要传迭代器。map/multimap存储pair<const Key,T>;key 不可修改,value 可以修改。map::operator[]三合一:key 不存在就插入;key 存在读取 / 修改 value;只查询不要用 \[\],会插入垃圾数据,优先 find。multimap支持重复 key,没有 operator \[\]。- insert 返回
pair<iterator,bool>,first 是迭代器,second 标记插入成功与否。
面试简答
Q:set 的迭代器为什么不能修改元素?
A:set 底层红黑树,修改 key 会破坏红黑树有序规则,所以迭代器指向内容是 const。
Q:map 的 operator \[\] 做了什么?
A:调用 insert,key 不存在插入默认 value;key 存在拿到原有结点;返回 value 的引用,用于读取或者修改。
Q:set::erase 传迭代器 和传值对于 multiset 有什么区别?
A:erase (迭代器):只删除这一个结点;erase (值) 会删除容器中所有等于该值的结点。
