C++ STL:map 与 set 详解------从底层红黑树到实际应用
文章目录
- [C++ STL:map 与 set 详解------从底层红黑树到实际应用](#C++ STL:map 与 set 详解——从底层红黑树到实际应用)
- 二、set
-
- [1 set 的基本特点](#1 set 的基本特点)
- [2 set 的底层结构](#2 set 的底层结构)
- [三、set 的构造与遍历](#三、set 的构造与遍历)
-
- [1 默认构造](#1 默认构造)
- [2 初始化列表构造](#2 初始化列表构造)
- [3 迭代器区间构造](#3 迭代器区间构造)
- [4 拷贝构造](#4 拷贝构造)
- [四、set 的迭代器](#四、set 的迭代器)
-
- [set 迭代器不能修改元素](#set 迭代器不能修改元素)
- [五、set 的插入](#五、set 的插入)
-
- [insert 的返回值](#insert 的返回值)
- [六、set 的查找](#六、set 的查找)
-
- [1 find](#1 find)
- [2 count](#2 count)
- [七、set 的删除](#七、set 的删除)
-
- [1 根据迭代器删除](#1 根据迭代器删除)
- [2 根据值删除](#2 根据值删除)
- [3 删除一个范围](#3 删除一个范围)
- [八 lower_bound 与 upper_bound](#八 lower_bound 与 upper_bound)
-
- lower_bound
- upper_bound
- [配合 erase 删除区间](#配合 erase 删除区间)
- [九 set 与算法库 find 的区别](#九 set 与算法库 find 的区别)
- [十 multiset](#十 multiset)
-
- [1 multiset 的 count](#1 multiset 的 count)
- [2 multiset 的 find](#2 multiset 的 find)
- [3 multiset 的 erase](#3 multiset 的 erase)
- [十一 set 的典型应用](#十一 set 的典型应用)
-
- [1 数组去重](#1 数组去重)
- [2 求两个数组的交集](#2 求两个数组的交集)
- [十二 set 解决环形链表问题](#十二 set 解决环形链表问题)
- [十三 map](#十三 map)
-
- [1 map 的基本概念](#1 map 的基本概念)
- [十四 map 的底层结构](#十四 map 的底层结构)
- [十五 pair](#十五 pair)
- [十六 map 的 value_type](#十六 map 的 value_type)
- [十七 map 的遍历](#十七 map 的遍历)
- [十八 map 为什么可以修改 value 但不能修改 key](#十八 map 为什么可以修改 value 但不能修改 key)
- [十九 map 的插入](#十九 map 的插入)
-
- [1 插入 pair](#1 插入 pair)
- [2 insert 的返回值](#2 insert 的返回值)
- [二十 map 的 find](#二十 map 的 find)
- [二十一 map 的 erase](#二十一 map 的 erase)
- [二十二 map 的 operator\[\]](#二十二 map 的 operator[])
-
- [1 key 不存在](#1 key 不存在)
- [2 key 已经存在](#2 key 已经存在)
- [二十三 operator\[\] 的底层理解](#二十三 operator[] 的底层理解)
- [二十四 map 统计元素出现次数](#二十四 map 统计元素出现次数)
- [二十五 find + iterator 实现统计](#二十五 find + iterator 实现统计)
- [二十六 map 中 \[\] 的三种典型用途](#二十六 map 中 [] 的三种典型用途)
-
- [1 插入](#1 插入)
- [2 修改](#2 修改)
- [3 查找](#3 查找)
- [二十七 multimap](#二十七 multimap)
-
- [multimap 为什么没有 operator\[\]](#multimap 为什么没有 operator[])
- [二十八 set 与 map 的联系](#二十八 set 与 map 的联系)
- [二十九 map 的典型应用:建立映射](#二十九 map 的典型应用:建立映射)
- [三十 map 解决复杂指针映射问题](#三十 map 解决复杂指针映射问题)
- [三十一 map 配合排序解决 Top K 问题](#三十一 map 配合排序解决 Top K 问题)
- [三十二 stable_sort 的使用场景](#三十二 stable_sort 的使用场景)
- [三十三 priority_queue 配合 map](#三十三 priority_queue 配合 map)
- [三十四 set 与 map 的选择](#三十四 set 与 map 的选择)
-
-
- 只关心数据是否存在
- [需要 key 对应 value](#需要 key 对应 value)
- [允许 key 重复](#允许 key 重复)
-
- [三十五 一个非常重要的理解](#三十五 一个非常重要的理解)
一、序列式容器与关联式容器
STL 中的容器大致可以分成两类
- 序列式容器
- 关联式容器
之前接触过的 string、vector、list、deque、array、forward_list 等都属于序列式容器
序列式容器的特点是元素按照一定的线性顺序组织起来,元素之间主要体现的是前后位置关系
例如
cpp
vector<int> v = {1, 2, 3, 4, 5};
这里主要关注的是元素在容器中的位置以及顺序
而关联式容器更加关注元素之间的关联关系以及关键字
常见的关联式容器包括
cpp
set
multiset
map
multimap
unordered_set
unordered_multiset
unordered_map
unordered_multimap
本篇主要学习
cpp
set
multiset
map
multimap
其中 set 和 map 的底层使用红黑树实现
红黑树本质上是一棵平衡二叉搜索树,因此能够保证较稳定的查找效率
二、set
1 set 的基本特点
set 可以理解为一个有序且不允许重复元素的集合
例如
cpp
set<int> s;
s.insert(5);
s.insert(2);
s.insert(7);
s.insert(5);
最终容器中的数据为
text
2 5 7
虽然 5 插入了两次,但是最终只保存一份
因此 set 有两个非常重要的特点
1 去重
2 有序
默认情况下按照升序排列
cpp
set<int> s = {5, 2, 7, 1, 3};
遍历结果为
text
1 2 3 5 7
2 set 的底层结构
set 底层使用红黑树实现
红黑树属于平衡二叉搜索树
因此 set 的主要操作通常具有
text
O(logN)
的时间复杂度
例如
cpp
s.insert(x);
s.find(x);
s.erase(x);
这些操作都能够利用红黑树的性质快速定位数据
另外 set 的迭代器遍历本质上是对红黑树进行中序遍历
二叉搜索树进行中序遍历得到的结果天然有序
所以
cpp
for (auto e : s)
{
cout << e << " ";
}
得到的就是按照 key 排序后的结果
三、set 的构造与遍历
1 默认构造
cpp
set<int> s;
创建一个空的 set
2 初始化列表构造
cpp
set<int> s = {5, 2, 7, 2, 8};
最终
text
2 5 7 8
重复数据会自动去除
3 迭代器区间构造
cpp
vector<int> v = {5, 2, 7, 2, 8};
set<int> s(v.begin(), v.end());
会将 [v.begin(), v.end()) 范围中的数据插入 set
同时自动完成排序和去重
4 拷贝构造
cpp
set<int> s1 = {1, 2, 3};
set<int> s2(s1);
s2 是 s1 的一份拷贝
四、set 的迭代器
set 支持正向迭代器和反向迭代器
cpp
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
也可以使用范围 for
cpp
for (auto e : s)
{
cout << e << " ";
}
反向遍历
cpp
for (auto it = s.rbegin(); it != s.rend(); ++it)
{
cout << *it << " ";
}
set 迭代器不能修改元素
例如
cpp
set<int> s = {1, 2, 3};
auto it = s.begin();
*it = 10;
这是错误的
原因是 set 中的元素本身就是红黑树的 key
如果允许直接修改 key
那么就可能破坏红黑树的有序结构
例如原来的结构满足
text
左边 < 根 < 右边
如果直接把某个节点的 key 修改成完全不同的值,就可能不再满足二叉搜索树的性质
所以
cpp
set<int>::iterator
指向的实际上可以理解成
cpp
const int
也就是说
set 不允许通过迭代器修改元素
五、set 的插入
最常见的插入方式
cpp
set<int> s;
s.insert(5);
s.insert(2);
s.insert(7);
重复插入不会产生新的元素
cpp
s.insert(5);
s.insert(5);
s.insert(5);
最终仍然只有一个 5
insert 的返回值
单个元素插入时
cpp
pair<iterator, bool> ret = s.insert(5);
返回值是一个 pair
其中
cpp
ret.first
表示指向相关节点的迭代器
cpp
ret.second
表示插入是否成功
例如
cpp
auto ret = s.insert(5);
if (ret.second)
{
cout << "插入成功" << endl;
}
else
{
cout << "元素已经存在" << endl;
}
如果 5 原来不存在
text
second == true
如果 5 原来已经存在
text
second == false
这个返回值设计非常实用
六、set 的查找
1 find
cpp
auto pos = s.find(x);
如果找到
cpp
pos != s.end()
如果没有找到
cpp
pos == s.end()
例如
cpp
auto pos = s.find(5);
if (pos != s.end())
{
cout << "找到了" << endl;
}
else
{
cout << "不存在" << endl;
}
2 count
对于 set 来说,一个值最多只能存在一次
所以
cpp
s.count(x);
返回值只有两种情况
text
0
1
因此可以直接用来判断元素是否存在
cpp
if (s.count(x))
{
cout << "存在" << endl;
}
七、set 的删除
1 根据迭代器删除
cpp
auto pos = s.find(x);
if (pos != s.end())
{
s.erase(pos);
}
2 根据值删除
cpp
s.erase(x);
如果 x 存在,就删除它
如果不存在,则什么也不做
返回值表示删除了几个元素
对于 set 来说最多删除一个,所以返回值为
text
0 或 1
例如
cpp
int ret = s.erase(x);
if (ret == 0)
{
cout << "不存在" << endl;
}
3 删除一个范围
cpp
s.erase(first, last);
删除的是
text
[first, last)
这个区间
八 lower_bound 与 upper_bound
这两个接口在有序关联式容器中非常重要
lower_bound
cpp
s.lower_bound(x);
返回第一个大于等于 x的位置
例如
text
10 20 30 40 50 60 70
执行
cpp
s.lower_bound(35);
返回 40
执行
cpp
s.lower_bound(40);
返回 40
所以可以记成
text
lower_bound >= x
upper_bound
cpp
s.upper_bound(x);
返回第一个大于 x的位置
例如
text
10 20 30 40 50 60 70
执行
cpp
s.upper_bound(40);
返回 50
所以可以记成
text
upper_bound > x
配合 erase 删除区间
例如
cpp
set<int> s = {10, 20, 30, 40, 50, 60, 70, 80};
auto left = s.lower_bound(30);
auto right = s.upper_bound(60);
s.erase(left, right);
删除的就是
text
30 40 50 60
最终剩下
text
10 20 70 80
这里非常重要的一点是
cpp
lower_bound(30)
得到的是 >= 30
cpp
upper_bound(60)
得到的是 > 60
因此 [left, right) 恰好包含
text
30 40 50 60
九 set 与算法库 find 的区别
对于 set
可以使用算法库中的
cpp
find(s.begin(), s.end(), x);
也可以使用 set 自己提供的
cpp
s.find(x);
两者最大的区别在于查找效率
算法库 find 本质上按照迭代器一个一个向后找
时间复杂度为
text
O(N)
而 set::find 利用底层红黑树进行搜索
时间复杂度为
text
O(logN)
所以如果容器本身提供了针对自身结构优化的查找接口,一般优先使用容器自己的接口
十 multiset
multiset 和 set 的使用方式基本一致
最大的区别是
text
set 不允许 key 重复
multiset 允许 key 重复
例如
cpp
multiset<int> s = {4, 2, 7, 2, 4, 8, 4};
遍历结果类似
text
2 2 4 4 4 7 8
依然有序,但是不会去重
1 multiset 的 count
因为允许重复
cpp
s.count(x);
返回的是 x 实际存在的数量
例如
cpp
multiset<int> s = {1, 2, 2, 2, 3};
cout << s.count(2);
结果为
text
3
2 multiset 的 find
如果一个值存在多个
cpp
auto pos = s.find(x);
找到的是中序遍历过程中遇到的第一个 x
可以继续向后遍历
cpp
while (pos != s.end() && *pos == x)
{
cout << *pos << " ";
++pos;
}
3 multiset 的 erase
这一点非常容易和 set 混淆
cpp
s.erase(x);
对于 multiset 来说,会删除所有值为 x 的元素
如果只想删除其中一个,可以先找到迭代器
cpp
auto pos = s.find(x);
if (pos != s.end())
{
s.erase(pos);
}
此时只删除 pos 指向的一个元素
十一 set 的典型应用
1 数组去重
如果有一个数组
cpp
vector<int> nums = {1, 2, 3, 2, 4, 1, 5};
可以直接构造
cpp
set<int> s(nums.begin(), nums.end());
得到
text
1 2 3 4 5
同时完成
text
去重
排序
2 求两个数组的交集
例如
text
nums1 = {1, 2, 2, 3}
nums2 = {2, 3, 4}
可以先分别放入两个 set
cpp
set<int> s1(nums1.begin(), nums1.end());
set<int> s2(nums2.begin(), nums2.end());
由于 set 已经完成排序
就可以利用两个有序序列的特点进行比较
核心思路
text
如果 *it1 < *it2
it1++
如果 *it1 > *it2
it2++
如果相等
找到交集
it1++
it2++
这本质上就是利用有序数据进行双指针遍历
十二 set 解决环形链表问题
set 不仅能够处理普通数据,还可以保存指针
例如
cpp
set<ListNode*> s;
可以用来记录已经访问过的节点
遍历链表时
cpp
ListNode* cur = head;
while (cur)
{
auto ret = s.insert(cur);
if (ret.second == false)
{
return cur;
}
cur = cur->next;
}
逻辑非常简单
第一次访问节点
cpp
s.insert(cur);
插入成功
如果以后再次遇到相同节点
cpp
s.insert(cur);
插入失败
说明这个节点之前已经出现过
于是可以判断链表存在环
这种方法利用 set 保存访问记录,把原本需要分析指针关系的问题转化成了一个非常直观的查重问题
十三 map
1 map 的基本概念
map 和 set 最大的区别在于
text
set key
map key + value
set 可以理解成
text
只有关键字
而 map 保存的是
text
键值对
例如
cpp
map<string, string> dict;
可以保存
text
apple 苹果
banana 香蕉
其中
text
string -> key
string -> value
十四 map 的底层结构
map 同样使用红黑树实现
红黑树按照 key 进行组织
因此
cpp
map<string, int>
中真正决定节点位置的是 string 类型的 key
而 value 只是与 key 绑定的数据
主要操作的时间复杂度通常为
text
O(logN)
遍历时同样按照红黑树中序遍历
所以 map 默认按照 key 升序遍历
十五 pair
map 中最重要的一个类型就是
cpp
pair
一个 pair 可以保存两个相关的数据
cpp
pair<string, int> p;
其中
cpp
p.first
表示第一个数据
cpp
p.second
表示第二个数据
例如
cpp
pair<string, int> p("张三", 18);
那么
cpp
p.first
就是
text
张三
而
cpp
p.second
就是
text
18
make_pair
也可以使用
cpp
auto p = make_pair("张三", 18);
得到一个 pair
十六 map 的 value_type
map<Key, T> 的底层节点存储的是一个键值对
可以理解为
cpp
pair<const Key, T>
例如
cpp
map<string, int>
内部对应的数据类型可以理解为
cpp
pair<const string, int>
这里 Key 前面的 const 非常重要
因为
text
key 不能修改
value 可以修改
十七 map 的遍历
例如
cpp
map<string, string> dict =
{
{"left", "左边"},
{"right", "右边"},
{"insert", "插入"}
};
使用迭代器遍历
cpp
for (auto it = dict.begin(); it != dict.end(); ++it)
{
cout << it->first << ":" << it->second << endl;
}
这里
cpp
it->first
表示 key
cpp
it->second
表示 value
也可以写成
cpp
for (const auto& e : dict)
{
cout << e.first << ":" << e.second << endl;
}
十八 map 为什么可以修改 value 但不能修改 key
例如
cpp
map<string, int> m;
m["apple"] = 10;
这里
text
apple
是 key
text
10
是 value
可以修改
cpp
m["apple"] = 20;
但是不能这样修改 key
cpp
it->first = "banana";
这是错误的
因为 map 的红黑树是根据 key 建立搜索结构的
如果修改 key
就可能破坏红黑树的结构
而 value 不参与红黑树的排序
所以修改 value 不会破坏红黑树结构
十九 map 的插入
1 插入 pair
cpp
map<string, string> dict;
dict.insert({"left", "左边"});
也可以写成
cpp
pair<string, string> kv("left", "左边");
dict.insert(kv);
或者
cpp
dict.insert(make_pair("left", "左边"));
2 insert 的返回值
cpp
auto ret = dict.insert({"left", "左边"});
返回
cpp
pair<iterator, bool>
其中
cpp
ret.first
表示 key 所在节点的迭代器
cpp
ret.second
表示是否插入成功
如果 key 不存在
text
插入成功
second == true
如果 key 已经存在
text
插入失败
second == false
注意
即使插入失败
cpp
ret.first
依然指向已经存在的那个 key
因此 insert 不仅可以完成插入
还可以利用返回值完成查找
二十 map 的 find
cpp
auto ret = dict.find("left");
如果找到
cpp
ret != dict.end()
那么
cpp
ret->first
就是 key
cpp
ret->second
就是 value
例如
cpp
auto ret = dict.find("left");
if (ret != dict.end())
{
cout << ret->second << endl;
}
二十一 map 的 erase
map 的删除方式与 set 基本一致
根据迭代器删除
cpp
auto pos = dict.find("left");
if (pos != dict.end())
{
dict.erase(pos);
}
根据 key 删除
cpp
dict.erase("left");
也可以删除一个区间
cpp
dict.erase(first, last);
二十二 map 的 operator\[\]
map 中非常重要的一个接口就是
cpp
operator[]
例如
cpp
map<string, int> m;
m["apple"] = 10;
这里的 [] 并不只是简单的查找
它同时具有
text
查找
插入
修改
等功能
1 key 不存在
假设
cpp
map<string, int> m;
此时没有
text
apple
执行
cpp
m["apple"];
相当于发现 key 不存在
于是会插入一个新的键值对
text
apple -> int()
对于 int 来说
cpp
int()
就是
text
0
所以相当于插入
text
apple -> 0
然后返回这个 value 的引用
因此
cpp
m["apple"]++;
就可以变成
text
apple -> 1
2 key 已经存在
假设
cpp
m["apple"] = 10;
再次执行
cpp
m["apple"];
此时不会重新插入
而是找到已有的 key
并返回它对应 value 的引用
因此
cpp
m["apple"] = 20;
就是直接修改 value
二十三 operator\[\] 的底层理解
可以把 operator[] 的核心逻辑理解成
cpp
mapped_type& operator[](const key_type& k)
{
pair<iterator, bool> ret = insert({k, mapped_type()});
iterator it = ret.first;
return it->second;
}
这里最重要的是
cpp
insert({k, mapped_type()})
如果 key 不存在
就插入
text
key + 默认构造的 value
如果 key 已经存在
插入失败
但是返回的 ret.first 仍然指向原来的 key
最后
cpp
return it->second;
返回 value 的引用
所以才可以做到
cpp
m[key]++
或者
cpp
m[key] = value;
二十四 map 统计元素出现次数
这是 map 最经典的应用之一
例如
cpp
string arr[] =
{
"苹果",
"西瓜",
"苹果",
"香蕉",
"苹果"
};
希望统计每种水果出现了多少次
可以使用
cpp
map<string, int> countMap;
最方便的写法
cpp
for (const auto& str : arr)
{
countMap[str]++;
}
为什么这一句就可以完成统计
第一次遇到
text
苹果
执行
cpp
countMap["苹果"]++;
因为 key 不存在
map 自动创建
text
苹果 -> 0
然后执行 ++
变成
text
苹果 -> 1
第二次遇到苹果
key 已经存在
直接找到
text
苹果 -> 1
并将其修改成
text
苹果 -> 2
所以最终可以得到
text
苹果 -> 3
西瓜 -> 1
香蕉 -> 1
这个写法在统计频率时非常常见
二十五 find + iterator 实现统计
不用 operator[] 也可以实现
cpp
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++;
}
}
逻辑就是
text
key 不存在
插入 key 和 1
key 存在
value++
这种写法能够更清晰地体现 find 和迭代器修改 value 的过程
而
cpp
countMap[str]++;
则更加简洁
二十六 map 中 \[\] 的三种典型用途
1 插入
cpp
dict["insert"];
如果 key 不存在
就会创建
text
insert -> 默认值
2 修改
cpp
dict["left"] = "左边";
如果 key 不存在
先插入
再修改
如果 key 已经存在
直接修改 value
3 查找
cpp
cout << dict["left"];
如果 key 已经存在
直接获取 value
因此 [] 是一个功能非常丰富的接口
二十七 multimap
multimap 和 map 的主要区别是
text
map key 不允许重复
multimap key 允许重复
例如
cpp
multimap<string, int> m;
m.insert({"apple", 1});
m.insert({"apple", 2});
m.insert({"apple", 3});
此时可以存在多个相同的 key
multimap 为什么没有 operator\[\]
这是一个非常重要的问题
对于 map
cpp
m[key]
可以明确表示
text
key 对应的唯一 value
因为一个 key 只能对应一个节点
但是 multimap 中
text
一个 key 可以对应多个 value
例如
text
apple -> 1
apple -> 2
apple -> 3
那么
cpp
m["apple"]
到底应该返回哪一个 value
因此 multimap 不提供 operator[]
二十八 set 与 map 的联系
可以把两者进行这样的理解
text
set
key
map
key + value
两者底层都是红黑树
两者都具有
text
有序
O(logN) 的增删查
支持迭代器
支持 lower_bound
支持 upper_bound
最大的区别就是数据组织方式不同
cpp
set<int>
节点主要保存
text
key
而
cpp
map<int, string>
节点保存
text
key + value
因此
text
set 更适合判断一个数据是否存在以及去重
map 更适合建立 key 与 value 之间的映射关系
二十九 map 的典型应用:建立映射
例如英文单词和中文含义
cpp
map<string, string> dict =
{
{"sort", "排序"},
{"insert", "插入"},
{"string", "字符串"}
};
查询
cpp
string str;
cin >> str;
auto ret = dict.find(str);
if (ret != dict.end())
{
cout << ret->second << endl;
}
else
{
cout << "无此单词" << endl;
}
这里 map 本质上就是建立
text
英文单词 -> 中文含义
的映射关系
三十 map 解决复杂指针映射问题
map 的另一个重要用途是建立两个对象之间的映射关系
例如复制带随机指针的链表时
可以建立
cpp
map<Node*, Node*> nodeMap;
这里
text
key = 原节点地址
value = 新节点地址
例如
text
原节点A -> 新节点A
原节点B -> 新节点B
原节点C -> 新节点C
创建复制节点时
cpp
nodeMap[cur] = copytail;
就把原节点和复制节点对应起来
当处理随机指针时
cpp
copy->random = nodeMap[cur->random];
就可以根据原节点的 random 指针快速找到对应的复制节点
这种思想非常重要
本质上就是
text
通过 map 建立对象之间的一一映射关系
三十一 map 配合排序解决 Top K 问题
map 经常负责统计
例如
cpp
map<string, int> countMap;
for (auto& e : words)
{
countMap[e]++;
}
这样就得到
text
单词 -> 出现次数
之后可以将 map 中的数据放入 vector
cpp
vector<pair<string, int>> v(countMap.begin(), countMap.end());
然后按照出现次数排序
cpp
sort(v.begin(), v.end(), Compare());
最后取前 k 个
这种思路可以拆成两个步骤
text
map
负责统计数据
vector / sort / priority_queue
负责根据统计结果筛选 Top K
三十二 stable_sort 的使用场景
当题目存在这样的要求
text
出现次数从高到低
如果出现次数相同
按照字典序从小到大
可以利用 map 本身按照 key 有序的特点
例如 map 中
text
apple 3
banana 3
orange 2
由于 key 已经按照字典序排列
对于出现次数相同的元素
text
apple
banana
本身已经满足字典序要求
这时可以使用稳定排序
cpp
stable_sort(v.begin(), v.end(), Compare());
让排序只主要按照次数进行排序
稳定排序能够保持相等元素原本的相对顺序
三十三 priority_queue 配合 map
也可以将 map 中的数据放入优先级队列
cpp
priority_queue<
pair<string, int>,
vector<pair<string, int>>,
Compare
> p(countMap.begin(), countMap.end());
通过仿函数控制优先级
例如题目要求
text
出现次数高的优先
次数相同
字典序小的优先
就需要同时考虑两个比较条件
cpp
return x.second < y.second ||
(x.second == y.second && x.first > y.first);
这里容易出错的地方在于 priority_queue 的比较规则和普通排序的直觉不完全一样
设计仿函数时一定要先明确
text
谁应该优先出队
再反过来设计比较关系
三十四 set 与 map 的选择
遇到问题时可以根据数据需求选择
只关心数据是否存在
cpp
set<int>
例如
text
去重
判断元素是否存在
记录访问过的对象
求集合关系
需要 key 对应 value
cpp
map<Key, Value>
例如
text
单词 -> 次数
单词 -> 中文含义
节点 -> 对应复制节点
编号 -> 对象
允许 key 重复
cpp
multiset
或者
cpp
multimap
三十五 一个非常重要的理解
使用 set 和 map 时,不要只把它们理解成普通容器
真正需要掌握的是它们背后的思想
text
set
通过 key 管理数据
自动排序
自动去重
map
通过 key 建立映射
key 自动排序
key 唯一
value 可以修改
而它们底层的红黑树保证了
text
查找
插入
删除
通常都能够保持
text
O(logN)
的效率
因此,当问题本身需要
text
有序
去重
快速查找
建立映射
统计次数
记录访问状态
时,set 和 map 往往能够非常直接地解决问题