深入红黑树:SGI-STL 中 map 与 set 的关联容器架构剖析

目录

  • 前言
  • 一、源码及框架分析
    • [1.1 源码文件分析](#1.1 源码文件分析)
    • [1.2 源码框架分析](#1.2 源码框架分析)
    • [1.4 源码命名缺陷](#1.4 源码命名缺陷)
    • [1.5 框架优势](#1.5 框架优势)
  • 结语

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、源码及框架分析

SGI-STL3.0 版本源代码,map 和 set 的源代码在map/set/stl_map.h/stl_set.h/stl_tree.h 等几个头文件中。

1.1 源码文件分析

对源码了解不多的兄弟们,我在这里说一下这几个头文件的包含关系:
一、核心层级与依赖关系

在 SGI-STL3.0 中,这几个头文件是分层依赖的结构,核心是「底层红黑树 → 容器封装 → 用户接口」:

cpp 复制代码
stl_tree.h (底层红黑树实现)
  ↓ 被依赖
stl_map.h / stl_set.h (基于红黑树封装 map/set 容器)
  ↓ 被包含
map / set (对外用户接口头文件)

二、逐文件解析
1. stl_tree.h:最底层的红黑树核心

  • 这是所有容器的基石,实现了完整的红黑树(rb_tree)数据结构:
    • 包含红黑树节点定义、颜色调整、旋转、插入 / 删除 / 查找等核心算法
    • 提供了红黑树的迭代器、分配器等基础设施
  • map 和 set 本质上都是红黑树的上层适配器,所以它们的底层逻辑完全复用 stl_tree.h 的代码,没有自己独立的树实现。

2. stl_map.h:map 容器的封装层

  • 基于 stl_tree.h 封装出 std::map 和 std::multimap:
    • 将红黑树的节点存储为 pair<const Key, T>(键值对,键不可修改)
    • 实现 operator[]、find、insert 等 map 特有的接口
    • 约束:键(Key)必须唯一,默认按升序排序
  • 它不实现树结构,只复用红黑树能力,把「键值对」适配到红黑树的节点存储模型。

3. stl_set.h:set 容器的封装层

  • 基于 stl_tree.h 封装出 std::set 和 std::multiset:
    • 将红黑树的节点存储为单个 Key 值(元素本身就是键)
    • 实现 insert、find 等接口,不提供 operator[](因为没有值,只有元素)
    • 约束:元素必须唯一,默认按升序排序
  • 同样是红黑树的适配器,把「单个元素」适配到红黑树的节点存储模型。

4. map / set:对外的用户接口头文件

  • 这两个是用户直接 #include 的头文件,内部逻辑很简单:
    • map 头文件:#include <stl_map.h> + 命名空间包装,让用户直接用 std::map
    • set 头文件:#include <stl_set.h> + 命名空间包装,让用户直接用 std::set
  • 作用:对用户隐藏底层 stl_tree.h 的复杂实现,只暴露容器的简洁接口。

总的来说 stl_tree.h 是底层红黑树引擎 ,stl_map.h 和 stl_set.h 是两个不同的方向盘 ,分别把红黑树改造成「键值对映射」和「元素集合」,而 map 和 set 是给用户握的方向盘外壳。本质上来说:map 和 set 是同一种树(红黑树)的两种不同用法,所以它们共享底层 stl_tree.h,只在上层封装逻辑上有区别。

所以SGI-STL 3.0 源码 (map/set 均基于stl_tree.h红黑树实现)

1.2 源码框架分析

下面我们就说一下map和set是怎么复用同一棵红黑树的,

一、 核心结构:三层封装与依赖关系

从图中可以清晰看到,map 和 set 并不是自己实现红黑树,而是 ** 组合(has-a)** 了一个红黑树对象。

1. 底层基石:rb_tree (红黑树核心)
文件 :stl_tree.h
职责:它是最底层的引擎。

  • 定义了 __rb_tree_node 节点结构,包含 value_field (存储数据) 和 parent/left/right 指针。
  • 实现了红黑树的插入insert(Value)、删除、旋转、查找find(Key)等核心算法逻辑。
  • 泛型设计 :rb_tree它的模板参数是 template <class Key, class Value...>。
    核心代码:typedef _rb_tree_node< Value > rb_tree_node;,也就是说,树的结点里只存Value,这里的 Value 决定了节点里存什么(是存 Key 还是 pair<const Key, T>)。这个底层红黑树只认两个东西,用Key排序/查找,在节点里存Value
  • 节点结构
cpp 复制代码
template <class Value>
struct __rb_tree_node {
    typedef __rb_tree_node<Value>* link_type;
    Value value_field; // 这里存的就是上面决定的 Value 类型
};

流程:map 告诉 rb_tree:Value 是 pair<const Key, T>。

rb_tree 实例化出 __rb_tree_node<pair<const Key, T>>。

节点内部的 value_field 就是一个键值对。

2. 中间封装层:map 容器类
文件 :stl_map.h
职责 :它是对 rb_tree 的适配器(Adapter)

  • 对外暴露接口:给用户提供 insert、find、erase 等成员函数。
  • 类型定义
    typedef Key key_type;排序用的键 = Key
    typedef pair<const Key, T> value_type;节点里存的东西 = 键+值 一整套(固定写法!),Key不能改,T可以改
  • 核心typedef rb_tree<key_type, value_type, ...> rep_type;,把「排序键」和「存储数据」传给红黑树,生成一个专属map的红黑树 → 起别名 rep_type
  • 组合关系:它内部都有一个 rb_tree 类型的成员变量 rep_type t(创建一台红黑树!这就是map的底层)。
  • map调用红黑树的方法:insert(value_type) { t.insert(...); } find(key_type) { return t.find(...); }
  • 举个例子 :map<int, string>
    Key = int(学号),Value = pair<const int, string>(学号 + 姓名)
    底层红黑树:用 int 排序,节点存 pair

3. 中间封装层:set 容器类
文件 :stl_set.h
职责 :它是对 rb_tree 的适配器(Adapter)

  • 对外暴露接口:给用户提供 insert、find、erase 等成员函数。
  • 类型定义
    typedef Key key_type;排序用的键 = Key
    typedef Key value_type;节点里存的东西 = Key(和键一模一样!)
  • 核心typedef rb_tree<key_type, value_type, ...> rep_type;传给红黑树:键和存的东西是同一个
  • 组合关系:它内部都有一个 rb_tree 类型的成员变量 rep_type t(创建一台红黑树!这就是set的底层)。
  • set调用红黑树的方法:insert(value_type) { t.insert(...); } find(key_type) { return t.find(...); },所有的 set::insert 底层调用的都是 rb_tree::insert,set::find 底层调用的都是 rb_tree::find。
  • 举个例子 :set< int >
    Key = int,Value = int
    底层红黑树:用 int 排序,节点存 int

4. 实际调用层:用户使用的 map / set

文件:标准头文件 < map > / < set >

职责:这些文件会包含 #include <stl_map.h> 并做简单的命名空间包装,让用户直接用 std::map。

总结图谱:

极简结构图

cpp 复制代码
【底层:stl_tree.h 红黑树】
   只做两件事:用Key排序 | 存Value
        ↓       套壳        ↓
【stl_map.h】               【stl_set.h】
传给红黑树:                传给红黑树:
Key = 键                    Key = 元素
Value = 键+值(pair)         Value = 元素
底层存:键值对              底层存:单一元素
对外:map容器               对外:set容器

要点补充 + 总结

  • 通过上面对框架的分析,我们可以看到源码中 rb_tree 用了一个巧妙的泛型思想实现,rb_tree 无论是实现 key 的搜索场景,还是 key/value 的搜索场景不是直接写死的,而是由第二个模板参数 Value 决定_rb_tree_node 中存储的数据类型。

  • set 实例化 rb_tree 时第二个模板参数给的是 key,map 实例化 rb_tree 时第二个模板参数给的是 pair<const key, T>,这样一颗红黑树既可以实现 key 搜索场景的 set,也可以实现 key/value 搜索场景的 map。

  • 要注意一下,源码里面模板参数是用 T 代表 value,而内部写的 value_type 不是我们日常 key/value 场景中说的 value,源码中的value_type 反而是红黑树结点中存储的真实的数据的类型。

  • rb_tree 第二个模板参数 Value 已经控制了红黑树结点中存储的数据类型,为什么还要传第一个模板参数 Key 呢?尤其是 set,两个模板参数是一样的,这是很多兄弟这时的一个疑问。要注意的是对于 map 和 set,find/erase 时的函数参数都是 Key,所以第一个模板参数是传给 find/erase 等函数做形参的类型的。对于 set 而言两个参数是一样的,但是对于 map 而言就完全不一样了,map insert 的是 pair 对象,但是 find 和 ease 的是 Key 对象。

1.4 源码命名缺陷

吐槽一下,这里源码命名风格比较乱,大佬有时写代码也不规范,乱弹琴。

  1. set 模板参数用的 Key 命名,map 用的是 Key 和 T 命名,而 rb_tree 用的又是 Key 和 Value

  2. value_type 双重含义
    用户视角 :在 map<int, string> 中,value_type 通常指 string(那个值)。
    源码视角 :在 rb_tree 和 set/map 的内部实现中,value_type 指代的是节点中存储的完整数据类型。对 map 来说,源码里的 value_type 是 pair<const int, string>。对 set 来说,源码里的 value_type 是 int。

1.5 框架优势

  1. 极致的代码复用(STL 的灵魂)
    只写 1 次红黑树代码,就能实现 4 个容器:map / set / multimap / multiset不用给每个容器单独写树,无冗余!
  2. 解决「查找」的痛点(最关键)
    map 查找:只需要传 key,不用传完整的 pair
    set 查找:传元素本身,红黑树因为分开了 Key 和 Value,两种查找都能支持
  3. 约束数据安全
    map 的 key 是 const:不能改(改了会乱序)
    set 的元素也是 const:不能改
    红黑树统一保证有序性

结语

相关推荐
人间打气筒(Ada)2 小时前
go:如何实现接口限流和降级?
开发语言·中间件·go·限流·etcd·配置中心·降级
福楠2 小时前
constexpr 全家桶
c语言·开发语言·c++
REDcker2 小时前
C++ vcpkg:安装、使用、原理与选型
开发语言·c++·windows·操作系统·msvc·vcpkg
晓13132 小时前
React篇——第五章 React Router实战
开发语言·javascript·ecmascript
小陈工2 小时前
2026年3月30日技术资讯洞察:AI算力突破、云原生优化与架构理性回归
开发语言·人工智能·python·云原生·架构·数据挖掘·wasm
feng_you_ying_li2 小时前
set/map的封装,底层为红黑树.
c++
古城小栈2 小时前
Tonic:构建高性能 Rust gRPC 服务
开发语言·rust
晨非辰2 小时前
Git版本控制速成:提交三板斧/日志透视/远程同步15分钟精通,掌握历史回溯与多人协作安全模型
linux·运维·服务器·c++·人工智能·git·后端
我是大猴子2 小时前
JAVA面试问题
开发语言·python