系列文章目录
提示:这里是系列文章的专栏
提示:以下是文章目录哦!
文章目录
目录
[1.1 两类容器概念区分](#1.1 两类容器概念区分)
[1.2 核心差异对比](#1.2 核心差异对比)
[1.3 map 与 set 底层本质](#1.3 map 与 set 底层本质)
[二、set 系列容器的使用](#二、set 系列容器的使用)
[2.1 set 和 multiset 参考文档](#2.1 set 和 multiset 参考文档)
[2.2 set 类核心介绍](#2.2 set 类核心介绍)
[2.2.1 模板参数](#2.2.1 模板参数)
[2.3 set 的构造与迭代器](#2.3 set 的构造与迭代器)
[2.3.1 常用构造方式](#2.3.1 常用构造方式)
[2.3.2 迭代器使用](#2.3.2 迭代器使用)
[2.4 set 的增删查核心接口](#2.4 set 的增删查核心接口)
[2.4.1 常用接口总览](#2.4.1 常用接口总览)
[2.5 insert 和迭代器遍历实战示例](#2.5 insert 和迭代器遍历实战示例)
[2.6 find 和 erase 实战示例](#2.6 find 和 erase 实战示例)
[2.7 lower_bound 与 upper_bound 区间删除](#2.7 lower_bound 与 upper_bound 区间删除)
[2.8 multiset 和 set 的核心差异](#2.8 multiset 和 set 的核心差异)
[三、map 系列容器的使用](#三、map 系列容器的使用)
[3.1 map 和 multimap 参考文档](#3.1 map 和 multimap 参考文档)
[3.2 map 类核心介绍](#3.2 map 类核心介绍)
[3.3 pair 类型详解](#3.3 pair 类型详解)
[3.4 map 的构造](#3.4 map 的构造)
[3.5 map 的增删查核心接口](#3.5 map 的增删查核心接口)
[3.6 map 的数据修改规则](#3.6 map 的数据修改规则)
[3.7 map 四种插入方式 + 遍历示例](#3.7 map 四种插入方式 + 遍历示例)
[3.8 map \[\] 运算符底层原理与使用](#3.8 map [] 运算符底层原理与使用)
[3.9 multimap 和 map 的核心差异](#3.9 multimap 和 map 的核心差异)
[四、封装红黑树实现 mymap 与 myset](#四、封装红黑树实现 mymap 与 myset)
[4.1 STL 源码框架分析](#4.1 STL 源码框架分析)
[4.2 前置准备:红黑树基础定义](#4.2 前置准备:红黑树基础定义)
[4.3 迭代器模拟实现](#4.3 迭代器模拟实现)
[四、end() 的实现方式](#四、end() 的实现方式)
[4.4 通用红黑树封装](#4.4 通用红黑树封装)
[4.5 模拟实现 myset](#4.5 模拟实现 myset)
[4.6 模拟实现 mymap](#4.6 模拟实现 mymap)
[5.1 set/multiset 对比](#5.1 set/multiset 对比)
[5.2 map/multimap 对比](#5.2 map/multimap 对比)
[5.3 底层核心设计回顾](#5.3 底层核心设计回顾)
[5.4 适用场景选型](#5.4 适用场景选型)
前言
提示:这里可以添加本文要记录的大概内容:
在前面的文章中,我们已经系统学习了二叉搜索树(BST)、AVL 树与红黑树这三种自平衡二叉搜索树的原理与实现。而在 C++ 的 STL 标准库中,map和set正是以红黑树为底层结构封装的关联式容器,它们是实际开发中解决高效查找、有序管理等问题的核心工具。
本文将从实际使用出发,完整讲解set/multiset与map/multimap的接口用法、特性差异与典型场景,并最终带你基于红黑树框架,从零模拟实现简化版的mymap与myset,打通从 "API 调用" 到 "底层原理" 的完整链路,真正理解 STL 关联式容器的设计思想。
提示:以下是本篇文章正文内容
一、序列式容器与关联式容器
1.1 两类容器概念区分
序列式容器
代表容器:string、vector、list、deque、array
- 逻辑结构:线性结构,元素按存储位置排列
- 元素关系:元素之间无关键字关联,交换位置不破坏容器结构
- 访问方式:按位置访问,支持随机 / 顺序遍历
关联式容器
代表容器:set/multiset、map/multimap、unordered_set/unordered_map
- 逻辑结构:非线性树形结构(红黑树 / 哈希表)
- 元素关系:元素依靠关键字 key关联,交换元素会破坏底层结构
- 访问方式:按关键字查找访问,自动排序
1.2 核心差异对比
| 特性 | 序列式容器 | 关联式容器(map/set) |
| 底层结构 | 线性数组 / 链表 | 红黑树(平衡二叉搜索树) |
| 查找效率 | O (N) 逐个遍历 | O (logN) 二分查找 |
| 元素顺序 | 由插入 / 存储位置决定 | 由 key 大小自动升序排序 |
| 核心用途 | 顺序存储、随机访问、尾部增删 | 快速查找、去重、键值映射、有序管理 |
|---|
1.3 map 与 set 底层本质
- set:纯 key 模型,只存储关键字,自动去重 + 排序
- map:key-value 键值对模型,key 唯一,映射对应 value
- 两者底层复用同一套红黑树,仅存储数据类型不同
下面我们来直观感受一下:
cpp
#include <iostream> // 标准输入输出头文件,用于cout、cin
#include <set> // STL set容器头文件
#include <map> // STL map容器头文件
#include <typeinfo> // 类型信息头文件,用于获取变量/对象的类型(typeid使用)
using namespace std;
int main()
{
// ------------------------------
// 1. 验证 set 的纯 key 模型
// set特点:元素唯一、自动排序、底层红黑树、元素只读不可修改
// ------------------------------
cout << "===== set 纯 key 模型验证 =====" << endl;
// 初始化set,自动去重+升序排序,重复的1会被过滤
set<int> s = {3, 1, 4, 1, 5, 9, 2, 6};
cout << "set 元素遍历(自动升序、无重复):" << endl;
// 范围for遍历set,输出中序遍历结果(有序)
for (auto e : s)
{
cout << e << " ";
}
// 输出set有效元素个数
cout << "\nset 实际元素个数:" << s.size() << endl;
// 验证 set 元素不可修改(解开注释会编译报错)
// 原因:set元素底层被const修饰,修改会破坏红黑树结构
// *s.begin() = 100;
// typeid获取迭代器解引用后的元素类型,name()输出类型名字
// 用于证明:set的元素是const类型,只读不可修改
cout << "set 元素类型:" << typeid(*s.begin()).name() << endl;
// ------------------------------
// 2. 验证 map 的 key-value 键值对模型
// map特点:key唯一、按key排序、key不可改、value可改、支持[]运算符
// ------------------------------
cout << "\n===== map 键值对模型验证 =====" << endl;
// 定义map:key是string(水果名),value是int(数量)
map<string, int> cnt;
// []运算符:key不存在则插入默认值0,再++;存在则直接++
cnt["苹果"]++;
cnt["西瓜"]++;
cnt["苹果"]++;
// 直接赋值:key不存在则插入,存在则修改value
cnt["香蕉"] = 10;
cout << "map 键值对遍历(key升序,key唯一):" << endl;
// 遍历map,kv是pair对象,kv.first=key,kv.second=value
for (auto& kv : cnt)
{
cout << kv.first << " : " << kv.second << endl;
}
// find查找key为"苹果"的节点,返回对应迭代器
auto it = cnt.find("苹果");
// 迭代器不等于end(),说明找到
if (it != cnt.end())
{
// it->first是key,被const修饰,不能修改(解开注释编译报错)
// it->first = "桃子";
// it->second是value,可以任意修改
it->second = 999;
cout << "修改后苹果的 value:" << it->second << endl;
}
// ------------------------------
// 3. 验证底层红黑树特性:高效查找 O(logN)
// set/map的find是红黑树二分查找,远快于线性遍历
// ------------------------------
cout << "\n===== 红黑树底层特性验证 =====" << endl;
// 在set中查找元素5,找到返回对应迭代器
if (s.find(5) != s.end())
cout << "set 中找到元素 5(红黑树 O(logN) 查找)" << endl;
// 在map中查找key为"香蕉"的节点
if (cnt.find("香蕉") != cnt.end())
cout << "map 中找到 key 为香蕉的节点(红黑树 O(logN) 查找)" << endl;
return 0;
}
运行结果如下:

二、set 系列容器的使用
2.1 set 和 multiset 参考文档
官方文档:https://legacy.cplusplus.com/reference/set/
2.2 set 类核心介绍
2.2.1 模板参数

- T:存储的关键字类型
- Compare:比较规则,默认
less<T>升序,可自定义仿函数改为降序 - Alloc:空间配置器,默认即可,一般无需手动传参
核心特性
- 元素唯一不重复,插入重复值会自动失效
- 底层红黑树,增删查效率 O(logN)
- 迭代器遍历为中序遍历,默认按 key 升序输出
- 迭代器只读,不允许修改元素(修改会破坏二叉搜索树结构)
2.3 set 的构造与迭代器
2.3.1 常用构造方式
这里的例子讲的很清楚,大家看一眼就可以明白

2.3.2 迭代器使用
set 支持正向迭代器、反向迭代器、const 迭代器 ,属于双向迭代器,只支持 ++/--,不支持随机访问


2.4 set 的增删查核心接口
2.4.1 常用接口总览

2.5 insert 和迭代器遍历实战示例
我们直接看例子:
cpp
#include <iostream>
#include <set>
#include <string>
using namespace std;
int main()
{
// 1. 默认升序插入、去重
set<int> s;
s.insert(5);
s.insert(2);
s.insert(7);
s.insert(5); // 重复插入,无效
// 迭代器遍历
auto it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
// 2. 初始化列表批量插入
s.insert({2,8,3,9});
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
// 3. string类型按ASCII码排序
set<string> strset = {"sort", "insert", "add"};
for (auto& e : strset)
{
cout << e << " ";
}
return 0;
}
运行结果如下:

2.6 find 和 erase 实战示例
我们直接来看例子:
cpp
#include <iostream>
#include <set>
#include <algorithm> // 算法库find
using namespace std;
int main()
{
set<int> s = {4,2,7,2,8,5,9};
for (auto e : s) cout << e << " ";
cout << endl;
// 1. 删除最小值(迭代器删除)
s.erase(s.begin());
for (auto e : s) cout << e << " ";
cout << endl;
// 2. 按值删除
int x = 7;
int num = s.erase(x);
if (num == 0)
cout << x << " 不存在!" << endl;
else
cout << "删除成功" << endl;
// 3. 先查找再删除(推荐)
cin >> x;
auto pos = s.find(x);
if (pos != s.end())
s.erase(pos);
else
cout << x << " 不存在!" << endl;
// 4. set自带find O(logN) VS 算法库find O(N)
auto pos1 = find(s.begin(), s.end(), x); // 遍历查找
auto pos2 = s.find(x); // 红黑树高效查找
// 5. count间接判断元素是否存在
if (s.count(x))
cout << x << " 存在!" << endl;
else
cout << x << " 不存在!" << endl;
return 0;
}
结果如下:

2.7 lower_bound 与 upper_bound 区间删除
这里一定要注意一下这两个函数的范围
我们直接来看例子:

运行结果如下: 
2.8 multiset 和 set 的核心差异
- set:元素唯一,不允许重复
- multiset:允许元素冗余重复,仅排序不去重
- find:multiset 返回中序第一个匹配元素
- count:multiset 返回元素实际个数
- erase:按值删除时,multiset 会删除所有匹配元素
我们直接来看例子:

运行结果如下:

三、map 系列容器的使用
3.1 map 和 multimap 参考文档
官方文档:https://legacy.cplusplus.com/reference/map/
3.2 map 类核心介绍
模板参数

- Key:关键字类型,唯一不可重复
- T:映射的 value 数据类型
- Compare:key 的比较规则,默认升序
- 底层存储:
pair<const Key, T>键值对
核心特性
- 存储key-value 键值对,key 唯一,value 可重复
- 底层红黑树,按key 自动升序排序
- 可修改 value,禁止修改 key(破坏树结构)
- 增删查效率稳定 O(logN)
3.3 pair 类型详解
map 底层节点依赖 pair 存储键值对,是 C++ 内置键值对结构体(这个我们之前讲过,所以这里不详细展开来讲解)
底层源码简化

使用方式

这里提一嘴:
make_pair 就是用来快速创建一个 pair 对象的工具函数
可以理解成:

不使用 make_pair(麻烦写法)
必须写清楚类型:

使用 make_pair(懒人写法)
不用写类型,自动推导

3.4 map 的构造

和set一样都是这四种构造方式,一定要牢记
3.5 map 的增删查核心接口
接口与 set 高度相似,仅插入为键值对,查找删除只需要传 key

这里强调一下,免得下面3.7插入看不懂
1. map 的本质:存的就是 pair
std::map<Key, Value> 里每个元素的类型是:

.first→ 键(const,不能改).second→ 值

2. insert 的参数类型就是 value_type(即 pair)
map 里有个别名:

而 insert 的核心重载是:

因此insert可以接受pair类型的数据
3.6 map 的数据修改规则
- 不可修改 key:key 是 const 修饰,修改会编译报错且破坏红黑树结构
- 可修改 value:通过迭代器 /\[\] 运算符修改
- insert 返回值:
pair<iterator, bool>- bool:true = 插入成功,false=key 已存在
- iterator:指向当前 key 所在节点迭代器
3.7 map 四种插入方式 + 遍历示例
cpp
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main()
{
map<string, string> dict;
// 四种插入方式
// 方式1:创建pair对象插入
pair<string, string> kv1("first", "第一个");
dict.insert(kv1);
// 方式2:匿名pair对象插入
dict.insert(pair<string, string>("second", "第二个"));
// 方式3:make_pair插入
dict.insert(make_pair("sort", "排序"));
// 方式4:C++11列表初始化(最常用)
dict.insert({"auto", "自动的"});
// 迭代器遍历
map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
// it->first 访问key,it->second访问value
cout << it->first << " : " << it->second << endl;
++it;
}
cout << endl;
// 范围for遍历
for (const auto& e : dict)
{
cout << e.first << " : " << e.second << endl;
}
return 0;
}
运行结果如下:

3.8 map \[\] 运算符底层原理与使用
底层实现逻辑

- key 不存在:插入默认值,返回 value 引用,可赋值修改
- key 已存在:查找并返回已有 value 引用
我们直接来看例子:

经典场景:统计元素出现次数

运行结果如下:

3.9 multimap 和 map 的核心差异
- map:key唯一 ,支持
[]运算符 - multimap:允许 key 重复 ,不支持
[](无法确定哪个 value) - find:multimap 返回中序第一个匹配 key
- erase:按值删除会删除所有相同 key
四、封装红黑树实现 mymap 与 myset
4.1 STL 源码框架分析
- STL 中 map/set复用同一棵红黑树 rb_tree
- set:红黑树存储
Key,纯 key 搜索 - map:红黑树存储
pair<const Key, T>,键值对搜索 - 核心设计:通过模板参数 + 仿函数,让红黑树适配两种存储类型
4.2 前置准备:红黑树基础定义
红黑树节点结构
前文已讲过,这里不过多赘述

4.3 迭代器模拟实现
一、核心思路
红黑树迭代器和 list 迭代器的实现思路一致:
- 本质 :用一个类封装红黑树节点指针,重载
*、->、++、--等运算符,让迭代器表现得像 "智能指针" - 核心目标 :实现红黑树的中序遍历(左子树 → 根节点 → 右子树) ,保证
map/set按 key 升序访问 begin():返回中序遍历的第一个节点(红黑树的最左节点)的迭代器
二、operator++(中序下一个节点)的核心逻辑
只看当前节点的局部关系,不用遍历整棵树,分两种情况:
-
当前节点有右子树
- 规则:当前节点访问完,下一个节点是右子树的最左节点
- 原因:中序遍历中,根节点之后是右子树的最左节点
- 实现:
cur = cur->_right; while (cur->_left) cur = cur->_left
-
当前节点没有右子树
- 规则:沿着
parent向上回溯,直到找到 "当前节点是父节点的左孩子" 的祖先节点 ,这个祖先就是下一个节点。 - 原因:当前节点是父节点的右孩子时,父节点已被访问;只有找到 "左孩子父节点",才说明左子树遍历完成,父节点就是下一个要访问的根节点。
- 实现:
while (parent && cur == parent->_right) { cur = parent; parent = parent->_parent; } cur = parent;
- 规则:沿着
三、operator--(中序前一个节点)的核心逻辑
和 ++ 完全对称,访问顺序反过来(右子树 → 根节点 → 左子树):
-
当前节点有左子树
- 下一个节点是左子树的最右节点
-
当前节点没有左子树
- 向上回溯,直到找到 "当前节点是父节点的右孩子" 的祖先节点 '',这个祖先就是前一个节点
四、end() 的实现方式
-
简化版实现(我们的手写版)
- 当
++遍历到最右节点后,继续++时,向上回溯找不到 "左孩子父节点",最终parent会变成nullptr。 - 此时将迭代器的节点指针置为
nullptr,用nullptr表示end()。 - 注意:反向迭代器
--end()时,需要特殊处理,直接指向最右节点。
- 当
-
STL 源码实现
- 红黑树会额外增加一个哨兵位头节点 作为
end()。 - 哨兵节点和根节点互为父节点,左孩子指向最左节点,右孩子指向最右节点。
- 这种实现更规范,也支持反向迭代器的统一处理。
- 红黑树会额外增加一个哨兵位头节点 作为

cpp
// 红黑树迭代器
template<class T, class Ref, class Ptr>
struct RBTreeIterator
{
typedef RBTreeNode<T> Node;
typedef RBTreeIterator<T, Ref, Ptr> Self;
Node* _node; // 当前节点
Node* _root; // 根节点
// 构造
RBTreeIterator(Node* node, Node* root)
: _node(node)
, _root(root)
{}
// 重载解引用
Ref operator*()
{
return _node->_data;
}
// 重载箭头
Ptr operator->()
{
return &_node->_data;
}
// 迭代器++ 中序下一个节点
Self& operator++()
{
if (_node->_right)
{
// 右子树存在,找右子树最左节点
Node* leftMost = _node->_right;
while (leftMost->_left)
leftMost = leftMost->_left;
_node = leftMost;
}
else
{
// 向上找祖先,直到当前节点是父节点的左孩子
Node* cur = _node;
Node* parent = cur->_parent;
while (parent && cur == parent->_right)
{
cur = parent;
parent = cur->_parent;
}
_node = parent;
}
return *this;
}
// 迭代器--
Self& operator--()
{
// 省略实现,和++逻辑相反
return *this;
}
// 判等
bool operator!=(const Self& s) const
{
return _node != s._node;
}
};
4.4 通用红黑树封装
cpp
// 红黑树类
template<class K, class T, class KeyOfT>
class RBTree
{
typedef RBTreeNode<T> Node;
public:
// 定义迭代器类型
typedef RBTreeIterator<T, T&, T*> Iterator;
typedef RBTreeIterator<T, const T&, const T*> ConstIterator;
// 获取起始、末尾迭代器
Iterator Begin()
{
Node* leftMost = _root;
while (leftMost && leftMost->_left)
leftMost = leftMost->_left;
return Iterator(leftMost, _root);
}
Iterator End()
{
return Iterator(nullptr, _root);
}
// 插入核心逻辑
pair<Iterator, bool> Insert(const T& data)
{
// 1. 空树直接插入根节点
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = BLACK;
return make_pair(Iterator(_root, _root), true);
}
// 2. 寻找插入位置
KeyOfT kot; // 仿函数取key
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (kot(cur->_data) < kot(data))
{
parent = cur;
cur = cur->_right;
}
else if (kot(cur->_data) > kot(data))
{
parent = cur;
cur = cur->_left;
}
else
{
// key重复,插入失败
return make_pair(Iterator(cur, _root), false);
}
}
// 3. 插入新节点
cur = new Node(data);
Node* newnode = cur;
if (kot(parent->_data) < kot(data))
parent->_right = cur;
else
parent->_left = cur;
cur->_parent = parent;
// 4. 红黑树颜色调整+旋转(省略旋转逻辑,沿用之前红黑树代码)
AdjustColor(newnode);
_root->_col = BLACK;
return make_pair(Iterator(newnode, _root), true);
}
// 查找节点
Iterator Find(const K& key)
{
KeyOfT kot;
Node* cur = _root;
while (cur)
{
if (kot(cur->_data) < key)
cur = cur->_right;
else if (kot(cur->_data) > key)
cur = cur->_left;
else
return Iterator(cur, _root);
}
return End();
}
private:
Node* _root = nullptr; // 根节点
void AdjustColor(Node* cur)
{
// 红黑树插入后颜色调整与旋转逻辑(复用之前实现)
}
};
4.5 模拟实现 myset
cpp
// 模拟实现set
namespace bit
{
template<class K>
class set
{
// 仿函数:从数据中取出key(set数据本身就是key)
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
// 迭代器别名
typedef typename RBTree<K, const K, SetKeyOfT>::Iterator iterator;
// 首尾迭代器
iterator begin() { return _t.Begin(); }
iterator end() { return _t.End(); }
// 插入
pair<iterator, bool> insert(const K& key)
{
return _t.Insert(key);
}
// 查找
iterator find(const K& key)
{
return _t.Find(key);
}
private:
// 底层复用红黑树:key=K,存储数据=const K
RBTree<K, const K, SetKeyOfT> _t;
};
}
4.6 模拟实现 mymap
cpp
// 模拟实现map
namespace bit
{
template<class K, class V>
class map
{
// 仿函数:从pair键值对中取出key
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
// 迭代器别名
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::Iterator iterator;
iterator begin() { return _t.Begin(); }
iterator end() { return _t.End(); }
// 插入键值对
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _t.Insert(kv);
}
// 查找
iterator find(const K& key)
{
return _t.Find(key);
}
// 重载[]运算符
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert(make_pair(key, V()));
return ret.first->second;
}
private:
// 底层红黑树:存储pair<const K, V>
RBTree<K, pair<const K, V>, MapKeyOfT> _t;
};
}
五、总结与对比
5.1 set/multiset 对比
| 容器 | 元素重复 | 排序 | find 返回 | count 返回 | 按值 erase |
| multiset | 允许 | 升序 | 第一个匹配 | 实际个数 | 删除所有匹配 |
| set | 不允许 | 升序 | 唯一元素 | 0 或 1 | 删除单个 |
|---|
5.2 map/multimap 对比
| 容器 | key 重复 | 支持 \[\] | 适用场景 |
| multimap | 允许 | 不支持 | 一对多映射 |
| map | 不允许 | 支持 | 一对一键值映射、去重统计 |
|---|
5.3 底层核心设计回顾
- map/set 底层均为红黑树,时间复杂度稳定 O(logN)
- 通过模板 + 仿函数实现红黑树复用,一套代码适配两种容器
- set 只读不可修改,map 可改 value 不可改 key
[]是 map 专属多功能接口:插入 + 查找 + 修改三合一
5.4 适用场景选型
- 只去重排序:选 set
- 允许重复排序:选 multiset
- 一对一键值映射:选 map
- 一对多键值映射:选 multimap
