C++ STL:map 与 set 详解——从底层红黑树到实际应用

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)
  • [九 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 的选择)
  • [三十五 一个非常重要的理解](#三十五 一个非常重要的理解)

一、序列式容器与关联式容器

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 往往能够非常直接地解决问题

相关推荐
行者全栈架构师1 小时前
【鸿蒙心迹】鸿蒙网络请求架构实战——@ohos.net.http 到 Axios 封装、拦截器与统一错误处理(HarmonyOS 7.x)
前端·算法·架构
Tri_Function1 小时前
背包:从入门到实战
c++
j7~1 小时前
【C++ 标准项目】发布订阅消息队列 (篇七):项目创建与基础工具模块搭建
开发语言·c++·log4j·uuid·protobuf·helper工具·消息定义类型和交换机类型定义
小宋10211 小时前
OpenTelemetry GenAI可观测性实战:串起模型、工具、Token与错误
java·人工智能·算法·贪心算法
嫂子开门我是_我哥1 小时前
青少年抑郁、焦虑、失眠与自杀相关表现的症状联系:一篇网络分析论文所使用的算法深度解读
论文阅读·算法·论文笔记
字节跳动的猫1 小时前
LikeShop 商品评价体系二开:追评、晒图审核与评价标签筛选功能开发
运维·数据结构·数据库
渊鱼L1 小时前
CAD随机颗粒插件2D V2.0 更新说明
算法
Lazionr2 小时前
unordered_map和unordered_set的使用
开发语言·c++·算法
行者全栈架构师2 小时前
从 55% 到 6%:一个快餐营养规划器的算法迭代实录
后端·算法·架构