【C++】封装红黑树实现map和set容器(详解)
封装红黑树实现map和set容器
- 封装红黑树实现map和set容器
- github地址
- 前言
- 一、改造红黑树
- 1. 分析源码
- 红黑树(rb_tree)泛型设计思想解析
- set & map 实例化 rb_tree 时的差异
- 为什么红黑树需要两个模板参数(Key & Value)?
- 2. 改造实现代码整体框架
- 结点颜色的枚举类
- 红黑树的结点定义
- 红黑树架构设计
- 二、KeyOfT 萃取器修改 insert 中的 比较逻辑
- 红黑树架构设计
- SetKeyOfT
- MapKeyOfT
- 传入不同的仿函数实例化红黑树分析
- KeyOfT 对红黑树中insert的修改
- 三、迭代器与相关重载实现
- 1. 自定义树的iterator类
- 由 普通迭代器构造 const 迭代器
- 2. operator*
- 3. operator->
- 4. operator!=
- 5. operator==
- 6. operator++
- 7. operator--
- 四、红黑树的iterator以及const_itreator的封装
- 1. 封装iterator与const_iterator
- 2. begin 与 const_begin
- 3. end 与 const_end
- 五、实现Key不支持修改与容器的迭代器
- 1. set 借助 const 迭代器实现 Key 不支持修改
- 2. map 实现 Key 不支持修改
- 3. 封装 map 的迭代器
- begin与end函数
- 六、改造insert实现operator[]
- 1. operator[] 的介绍
- 2. operator[]和insert行为的哲学
- 3.红黑树中 insert 函数的修改
- 4. map中 operator[] 的实现
- 4. set 中 insert 的实现
- 实现 insert
- 由树的普通迭代器构造 const 迭代器 详解
- 七、完整代码实现
- m_set
- m_map
- 改造后的红黑树
- 八、结语
封装红黑树实现map和set容器
github地址
有梦想的电信狗
前言
前文链接:手搓红黑树详解
在前文《手搓红黑树详解》中,我们已经实现了一棵完整的红黑树。
本篇将进一步改造这棵红黑树,使它能够支撑 STL 风格的 map 与 set 容器。
STL 中的 map 和 set 虽然都基于红黑树实现,但两者的数据结构并不相同:
set只保存键key;map保存键值对pair<const K, V>。
为了兼容两种场景,标准库采用了泛型模板和萃取器 (KeyOfT) 的设计思想,使红黑树在编译期即可灵活适配不同容器。
本文将带你一步步封装出一个可运行的 map 与 set,深入理解它们与红黑树之间的联系,以及 STL 容器设计的核心思路。
一、改造红黑树
1. 分析源码
set 与 map 的底层都是红黑树,但是我们不能直接使用一棵普通的红黑树套进去,因为 set 和 map 中所存储的数据类型是不一样的。
- set 中是单个值 key
- map 中是一个 pair 类型。那么我们应该如何解决呢?我们来参考一下 STL 库中的写法。
SGI-STL30 版本源代码,map 和 set 相关的实现代码分布在 map、set、stl_map.h、stl_set.h、stl_tree.h 等几个头文件 中。set 和 map 的实现结构框架核心部分截取出来如下:
// stl_set.h
template <class Key, class Compare = less<Key>, class Alloc = alloc>
class set
{
public:typedef Key key_type;typedef Key value_type;private:typedef rb_tree<key_type, value_type,identity<value_type>, key_compare, Alloc> rep_type;rep_type t; // red-black tree representing set};// stl_map.h
template <class Key, class T, class Compare = less<Key>, class Alloc = alloc>
class map
{
public:typedef Key key_type;typedef pair<const Key, T> value_type;private:typedef rb_tree<key_type, value_type,select1st<value_type>, key_compare, Alloc> rep_type;rep_type t; // red-black tree representing map};// stl_tree.h
template <class Key, class Value, class KeyOfValue, class Compare, class Alloc = alloc>
class rb_tree
{
protected:typedef void* void_pointer;typedef __rb_tree_node_base* base_ptr;typedef __rb_tree_node<Value> rb_tree_node;typedef rb_tree_node* link_type;protected:size_type node_count; // keeps track of size of treelink_type header;
};template <class Value>
struct __rb_tree_node : public __rb_tree_node_base
{typedef __rb_tree_node<Value>* link_type;Value value_field;
};
- 图解分析如下:

红黑树(rb_tree)泛型设计思想解析
通过上图对框架的分析,我们可以看到 SGI STL 源码中 rb_tree 用了一个巧妙的泛型思想实现
- 它不直接写死“仅支持key 搜索”或“仅支持key/value 搜索",而是通过红黑树第二个模板参数
Value灵活控制: - 红黑树节点(
_rb_tree_node)中实际存储的数据类型,由Value决定。这样一颗红黑树既可以实现 key 搜索场景的 set,也可以实现 key/value 搜索场景的 map。-
set 实例化 rb_tree 时,第二个模板参数给的是 key,
-
map 实例化 rb_tree 时,第二个模板参数给的是 pair。
-
set & map 实例化 rb_tree 时的差异
-
set 实例化时的场景:
- 当
set实例化rb_tree时,给第二个模板参数传入纯 key 类型(如:set<int>中,Value就是int) - 红黑树节点直接存储 key,自然适配“仅按 key 搜索、不重复存储”的需求
- 当
-
map 实例化时的场景:
- 当
map实例化rb_tree时,给第二个模板参数传入键值对类型pair<const Key,T>(如:map<int,string>中,Value是pair<const int, string>) - 红黑树节点存储完整的键值对pair,从而支持“按 key 关联 value ”的搜索逻辑
- 当

注意事项:关于
value_type的特殊含义
- 源码里的模板参数常用
T代表“节点存储的数据类型(即这里的Value)rb_tree内部定义的value_type,并非我们日常说的“key/value里的value”,而是红黑树节点实际存储的数据的类型(对set是key类型,对map是pair<const Key,T>类型)
为什么红黑树需要两个模板参数(Key & Value)?
既然 rb_tree 第二个模板参数 Value已经控制了红黑树节点中存储的数据类型,为什么还要传第一个模板参数 Key 呢?
尤其是 set,两个模板参数均为K,这是为什么呢?
核心原因在于 find/erase 等操作的参数需求:
-
对 set 和 map 来说:
find /erase函数的参数是Key类型(按key查找、删除),而非完整的节点数据(Value 类型) -
对
set而言:Key和Value类型相同(节点存key,操作也用 key),两个模板参数看似冗余,但是这样做主要是为了和map容器保持统一的接口
-
对
map而言:Key(操作入参类型)和Value(节点存储的键值对类型)完全不同——map插入的是pair对象,但查找、删除只用Key
因此:Key 模板参数的意义是为 find/erase 等函数提供形参类型,让一份红黑树模板代码通过不同的实例化,能统一支撑 set(Key 与Value 同类型)和map (Key 与 Value 不同类型)的存储场景。
简而言之:
SGI STL中的红黑树通过模板参数的分层职责(Value控制节点存储类型,Key控制操作入参类型),使rb_tree成为一套“万能骨架",向上完美适配set(纯key场景)和map(key/value 场景),体现了泛型编程的灵活性与复用性
2. 改造实现代码整体框架
结点颜色的枚举类
- 我们定义一个枚举类,枚举值表示结点的颜色
enum Colour {Red,Black
};
红黑树的结点定义
- 红黑树结点为模版实现
// 由于 set 和 map 底层存储的数据不一样, set 存储 key,map 存储 pair<key, value> (key-value)
// 所以红黑树结点存储的数据类型不是写死的, 而是写成一个模板参数 T
// 红黑树代码模板被实例化出两份,一份 _data 存储 K 类型,一份 _data 存储 pair<K, V> 类型
template<class T>
struct RBTreeNode
{RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;Colour _col;T _data;// 新结点默认是红色的RBTreeNode(const T& data):_left(nullptr),_right(nullptr),_parent(nullptr),_data(data),_col(Red){ }
};
-
搜索树常用于存储键值,方便查找关键字,这里我们使用
T _data来存储我们的实际数据- 在 set 中 T 为 Key
- 在 map 中T为 pair<K, V>
-
结点中的成员变量:采用三叉链的方式实现
RBTreeNode<T>* _left;:指向左孩子的指针RBTreeNode<T>* _right;:指向右孩子的指针RBTreeNode<T>* _parent;:指向父节点的指针
-
默认构造函数
RBTreeNode(const T& data):- 将三个指针初始化为
nullptr,初始化结点颜色为 Red - 使用形参
data初始化类内的_data成员
- 将三个指针初始化为
-
结点采用
struct设计,默认权限为public,方便下文的RBTree类访问成员
红黑树架构设计
template<class K, class T, class KeyOfT>
class RBTree
{
private:typedef RBTreeNode<T> Node;RBTreeNode<T>* _root = nullptr;
public:// ...迭代器和相关成员函数 实现
}
Node* _root = nullptr:初始时根节点为空typedef RBTreeNode<T> Node;:结点类型重定义,简化书写- 模板参数设置为
template<class K, class T, class KeyOfT>,后文解释
二、KeyOfT 萃取器修改 insert 中的 比较逻辑
由于红黑树(RBTree)采用泛型设计,无法直接判断模板参数 T 具体是单纯的键类型K(如:set的场景),还是键值对类型
pair<K,V>(如:map 的场景)
- 这会导致一个问题:在
insert的逻辑里进行“节点值大小比较”时,默认的比较规则无法满足需求- 默认比较规则:
set使用Key进行比较,map使用std::pair预设的比较规则进行比较
- 默认比较规则:
set的Key可以完成预期比较,而std::pair的默认支持的是key和value一起参与比较,不符合我们仅比较Key的比较逻辑
为解决这个问题,我们在 map 和 set 这两个容器层,分别实现了仿函数 MapKeyofT 和 SetKeyofT ,并将它们传递给红黑树的KeyOfT模板参数
这样,红黑树内部就能通过上层容器的 KeyOfT 仿函数:通过 KeyOfT 仿函数取出 T 类型对象中的 key,再进行比较
- 先从 T 类型对象中提取出 key,再用这个 key 进行比较,从而实现“仅按 key 排序/插入"的逻辑。
红黑树架构设计
- 模板参数设置为
template<class K, class T, class KeyOfT>,方便通过仿函数KeyOfT取出需要进行比较的Key值
template<class K, class T, class KeyOfT>
class RBTree
{
private:typedef RBTreeNode<T> Node;RBTreeNode<T>* _root = nullptr;
public:// ...迭代器和相关成员函数 实现
}
SetKeyOfT
// m_set.h
template<class K>
class set
{// 萃取器struct SetKeyOfT{const K& operator()(const K& key) const{return key;}};RBTree<K, K, SetKeyOfT> _tree;
public:// ...
}
MapKeyOfT
// m_map.h
template<class K, class V>
class map
{
private:// 萃取器 把 T 对象中的 key 取出来struct MapKeyOfT{const K& operator()(const pair<K, V>& kv) const{return kv.first;}};RBTree<K, pair<K, V>, MapKeyOfT> _tree;
public:// ...
};
传入不同的仿函数实例化红黑树分析

使用 萃取器 KeyOfT 取出 Key 的设计相当巧妙
set和map的设计,_data分别是Key和pair- 如果是
set中的Key, 可以直接比较,如果是map,标准库中的pair<K, V>的比较不支持仅比较K
因此设计了仿函数萃取器 KeyOfT ,用于取出 T 中的 Key 来进行比较
- 如果是
set返回 K,如果是map中的pair<K, V>,取出 K 进行比较 - 可以认为这里是
set迁就了map,因为set的 K 本身就可以直接比较,map的pair<K, V>需要借助KeyOfT取出Key进行比较
KeyOfT完全是为了map设计的,
set的SetKeyOfT仅仅是为了保持统一的接口风格
KeyOfT 对红黑树中insert的修改
需要修改的地方如下:
- 红黑树在插入节点时的比较逻辑需要修改
- 之前红黑树搜索插入位置时,进行的是直接比较
- 现在需要使用仿函数KeyOfT,先提取出类型中的Key再进行比较
- 例如:
if (data < curNode->_data))修改为if (kot(data) < kot(curNode->_data)),仅比较关键字Key,需要再套上一层仿函数对象- kot为定义出的仿函数对象
bool insert(const T& data)
{// 先走二叉搜索树的插入逻辑if (_root == nullptr){_root = new Node(data);_root->_col = Black; // 性质 根节点是黑色的return true;}// _root 不为空时,二叉搜索树的逻辑Node* parent = nullptr;Node* curNode = _root;// 先找空,找到一个可以插入的位置KeyOfT kot;while (curNode){if (kot(data) < kot(curNode->_data)){parent = curNode;curNode = curNode->_left;}else if (kot(data) > kot(curNode->_data)){parent = curNode;curNode = curNode->_right;}// 搜索树中不允许有重复的值 对于已有值,不插入elsereturn false;}// while 循环结束后,代表找到了可以插入的位置// 找到位置了,但父节点不知道 新结点比自己大还是比自己小curNode = new Node(data);if (kot(curNode->_data) < kot(parent->_data))parent->_left = curNode;elseparent->_right = curNode;curNode->_parent = parent;// ... 仅以上代码 有部分修改,其余代码和红黑树的插入一致
}
三、迭代器与相关重载实现
这里的 iterator 的实现思路与 list 的 iterator 框架一致:
- 用一个类封装 “结点指针”
- 再通过重载运算符,让迭代器能像指针一样完成访问和移动行为(如:
*it、++it、it->等行为)
1. 自定义树的iterator类
template<class T, class Ptr, class Ref>
struct __TreeIterator
{typedef RBTreeNode<T> Node;typedef __TreeIterator<T, Ptr, Ref> Self;typedef __TreeIterator<T, T*, T&> Iterator; // 该类型始终为常量迭代器Node* _node;__TreeIterator(Node* node):_node(node){ }// 由 普通迭代器构造 const 迭代器__TreeIterator(const Iterator& it):_node(it._node){ }
};
- 这里树的
iterator的实现思路与list的iterator实现思路一致,模板参数的控制思路也一致: - 构造函数
__TreeIterator(Node* node)使用结点的指针构造出一个迭代器类型
由 普通迭代器构造 const 迭代器
typedef __TreeIterator<T, Ptr, Ref> Self;
typedef __TreeIterator<T, T*, T&> Iterator; // 该类型始终为普通迭代器迭代器__TreeIterator(const Iterator& it):_node(it._node)
{ }
- 注意这里:
typedef __TreeIterator<T, T*, T&> Iterator中的模板参数给的是<T, T*, T&>,那么类型Iterator始终为普通迭代器 - 形参
const Iterator& it为普通迭代器的常引用,既可以接受普通迭代器,也可以接收 const 迭代器,该构造函数完成的功能有两个:- 由 普通迭代器构造 const 迭代器:当
it接收到的实参是 普通迭代器 时,完成由普通迭代器构造 const 迭代器 - 由 const 迭代器 的拷贝构造:当
it接收到的实参是 const 迭代器 时,完成 const 迭代器 的拷贝构造
- 由 普通迭代器构造 const 迭代器:当
2. operator*
Ref operator*() const
{if (_node == nullptr)assert(false);return _node->_data;
}
- 防止访问空结点:访问空结点时
assert(false) *it需要访问到结点中的数据,引用返回,因此返回结点中的数据return _node->_data
3. operator->
Ptr operator->() const
{if (_node == nullptr)assert(false);return &(_node->_data);
}
- 防止访问空结点:访问空结点时
assert(false) it->是通过指针访问数据的行为,因此返回结点中的地址return &(_node->_data)
4. operator!=
bool operator!=(const Self& s) const
{ return _node != s._node;
}
5. operator==
bool operator==(const Self& s) const
{ return _node == s._node;
}
6. operator++
关于
operator++的讲解见链接:https://sanqiucoder.blog.csdn.net/article/details/151373721?spm=1001.2014.3001.5502
- 前置++
Self& operator++()
{// 右树不为空,就访问右树的最左结点if (_node->_right){// 找右树的最左节点(最小节点)Node* subLeft = _node->_right;while (subLeft->_left)subLeft = subLeft->_left;_node = subLeft;}// 右树为空时分两种情况讨论else{Node* cur = _node;Node* parent = cur->_parent;// 找孩子是父亲左的那个祖先节点,就是下一个要访问的节点// 当前结点 是 父亲的 右时,才继续向上找while (parent && cur == parent->_right){cur = cur->_parent;parent = parent->_parent;}// 有两种情况会结束循环 // 1. cur == parent->_left 时, 下一个访问的就是 cur 的 父亲,直接 _node = parent// 2. parent 为 空 时,cur 此时为根节点, 且 cur 的右为空,此时中序遍历结束_node = parent;}return *this;
}
- 后置++
Self operator++(int)
{Self tmp(*this);++*this;return tmp;
}
7. operator–
关于
operator--的讲解见链接:https://sanqiucoder.blog.csdn.net/article/details/151373721?spm=1001.2014.3001.5502
- 前置–
Self& operator--()
{// -- 就是 ++ 反过来// 左子树不为空,就去找左树的最大结点if (_node->_left){Node* subRight = _node->_left;while (subRight->_right)subRight = subRight->_right;_node = subRight;}else{// 孩子是父亲的右的那个节点Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left){cur = cur->_parent;parent = parent->_parent;}_node = parent;}return *this;
}
- 后置–
Self operator--(int)
{Self tmp(*this);--*this;return tmp;
}
四、红黑树的iterator以及const_itreator的封装
1. 封装iterator与const_iterator
在树的层面实现迭代器:
template<class K, class T, class KeyOfT>
class RBTree
{
private:typedef RBTreeNode<T> Node;RBTreeNode<T>* _root = nullptr;public:// 同一个类模板 传不同的参数,实例化出不同的类型typedef __TreeIterator<T, T*, T&> iterator;typedef __TreeIterator<T, const T*, const T&> const_iterator;// ... begin end 等迭代器
}
在实现迭代器时,通过传不同的模板参数,实例化出不同的迭代器类型,即可控制是普通迭代器还是 const 版本的迭代器
typedef __TreeIterator<T, T*, T&> iterator:传入普通 T 类型,实例化出普通迭代器typedef __TreeIterator<T, const T*, const T&> const_iterator:传入const T类型,实例化出const迭代器
2. begin 与 const_begin
实现如下:
iterator begin()
{Node* leftMin = _root;while (leftMin && leftMin->_left)leftMin = leftMin->_left;return iterator(leftMin);
}// const 版本
const_iterator begin() const
{Node* leftMin = _root;while (leftMin && leftMin->_left)leftMin = leftMin->_left;return const_iterator(leftMin);
}
begin()返回中序遍历的第一个位置,因此找树的最左节点即可- 最终返回一个迭代器对象,用树的最左结点的指针构造迭代器对象返回
- 普通对象调用普通
begin(),返回iterator对象 const对象调用const begin(),返回const_iterator对象
- 普通对象调用普通
3. end 与 const_end
iterator end()
{return iterator(nullptr);
}const_iterator end() const
{return const_iterator(nullptr);
}
end()返回最后一个位置的下一个位置,这里我们用空结点来表示- 最终返回一个迭代器对象,用
nullptr构造空迭代器对象返回- 普通对象调用普通
end(),返回iterator对象 const对象调用const end(),返回const_iterator对象
- 普通对象调用普通
五、实现Key不支持修改与容器的迭代器
- map 和 set 的迭代器的实现,本质是对红黑树迭代器的适当封装,我们对其进行封装即可
- map 和 set 的普通迭代器,本质是对红黑树中 普通迭代器 的适当封装
- map 和 set 的 const 迭代器,本质是对红黑树中 const 迭代器 的适当封装
1. set 借助 const 迭代器实现 Key 不支持修改
我们先来观察 STL 中源码中的实现:截取部分如下
class set {
public:typedef Key key_type;typedef Key value_type;private:typedef rb_tree<key_type, value_type, identity<value_type>, key_compare, Alloc> rep_type;rep_type t; // red-black tree representing set
public:// 两种迭代器typedef typename rep_type::const_iterator iterator;typedef typename rep_type::const_iterator const_iterator;
}
- 可以看到,源码中普通迭代器和const迭代器的实现均为
const_iterator,源码中借助了const迭代器实现普通迭代器,我们仿照其实现即可
template<class K>
class set
{
public:typedef typename RBTree<K, K, SetKeyOfT>::const_iterator iterator;typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;const_iterator begin() const{return _tree.begin();}const_iterator end() const{return _tree.end();}// ...
}
注意 set 的普通迭代器和const迭代器都是 const_iterator,因此只需提供 const 版本的begin和end函数即可
- const版本的begin和end函数,普通对象和const对象都可以调用
分析 STL 中的实现如下:
- 因为
set的普通迭代器和const迭代器均为const_iterator,所以STL中只实现了const版本的begin和end函数,普通对象和const对象都可以调用const函数,都返回const_iterator
typedef typename rep_type::const_iterator iterator;
typedef typename rep_type::const_iterator const_iterator;
// 库里的写法,由 typedef 得知,set 中的 iterator 实际是 const_iterator
iterator begin() const { return t.begin(); }
iterator end() const { return t.end(); }
2. map 实现 Key 不支持修改
template<class K, class V>
class map
{
private:RBTree<K, pair<const K, V>, MapKeyOfT> _tree; // 将 pair 中的 K 设置为 const K 类型
public:// ...
}
map实现的是key不能修改,value能修改,我们可以在pair<key, value>的存储层解决这个问题,存储时,直接存储key为常量的pair<const K, V>。map中存储的树类型为:RBTree<K, pair<const K, V>, MapKeyOfT> _tree;即可实现map的key不能修改,value能修改
3. 封装 map 的迭代器
- map 和 set 的普通迭代器,本质是对红黑树中 普通迭代器 的适当封装
- map 和 set 的 const 迭代器,本质是对红黑树中 const 迭代器 的适当封装
template<class K, class V>
class map
{
private:RBTree<K, pair<const K, V>, MapKeyOfT> _tree;
public:// ... // key 不能修改, value 能修改typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::iterator iterator;typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;// ... begin 和 end 等函数
}
-
实现
map的key不能修改,value能修改是在pair的存储层增加了const来解决这个问题,因此begin和end常规实现即可- 普通 map 对象,调用树的普通迭代器
- const map 对象,调用树的 const 迭代器
-
map封装树的迭代器:- 将树中的普通迭代器封装为 map 的普通迭代器:
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::iterator iterator
- 将树中的 const迭代器封装为 map 的const迭代器:
typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator
- 将树中的普通迭代器封装为 map 的普通迭代器:
begin与end函数
map的Key不能修改是在 pair 的存储层实现的,因此需要分别实现begin和 end 函数的普通版本和const对象版本
// 普通 map 对象,调用树的普通迭代器
iterator begin()
{return _tree.begin();
}
iterator end()
{return _tree.end();
}const_iterator begin() const // const map 对象,调用树的普通迭代器
{return _tree.begin();
}
const_iterator end() const
{return _tree.end();
}
六、改造insert实现operator[]
1. operator[] 的介绍

在 map 中,operator[] 可以实现插入 + 修改的功能。既然有插入,那么就必定要用到 insert 接口。在 STL 库中,insert 的接口的返回值是一个 pair 类型:
- 如果插入成功,返回
<插入的该数据对应的迭代器, true>; - 如果插入失败,返回
<已经存在的该数据对应的迭代器, false>
2. operator[]和insert行为的哲学
map 中operator[]的行为等效如下:
mapped_type& operator[] (const key_type& k); // 参考文档中的声明
pair<iterator,bool> insert (const value_type& val); // 参考文档中 insert 的声明// operator[] 的行为等效如下
(*((this->insert(make_pair(mapped_type()))).first))
(*((this->insert()).first)) // 将 make_pair 函数去掉后如下
// 对以上代码做拆解,可得如下结果
(*((insert()).first)) // 这里本质是对 insert 返回的 pair 中的 first 进行解引用// 由insert的返回值为 pair ,将insert替换为pair,可以得到 以下结果
(* ( (pair<iterator,bool>).first) ) // 本质是对 insert 返回的迭代器进行解引用
insert之所以这么设计,就是为了实现 operator[]时可以复用insert
- 因此我们需要将红黑树中
insert的返回值修改为返回pair
3.红黑树中 insert 函数的修改
- 仅对 insert 函数中原有的 return 语句进行了修改
std::pair<iterator, bool> insert(const T& data)
{// 先走二叉搜索树的插入逻辑if (_root == nullptr){_root = new Node(data);_root->_col = Black; // 性质 根节点是黑色的//return true;return make_pair(iterator(_root), true);}// _root 不为空时,二叉搜索树的逻辑Node* parent = nullptr;Node* curNode = _root;// 先找空,找到一个可以插入的位置// 使用 萃取器 取出 Key 设计相当巧妙// 标准库中的 pair 不能直接比较大小,我们设计了 一个萃取器 用于取出 T 中的 Key// 可以认为这里是 set 迁就了 map ,因为set的 K 可以直接比较,map 的pair<K, V> 需要取出 Key// 这是 map 和 set 的设计,_data 可能是 K,也可能是 pair// 如果是 K,可以直接比较,如果是map,标准库中的 pair<K, V> 的比较对象不支持仅比较K// 因子设计了仿函数 KeyOfT 如果是 set 返回 K, 如果是map 中的 pair<K, V>,取出 K 进行比较// KeyOfT 完全是为了 map 设计的,KeyOfT kot;while (curNode){if (kot(data) < kot(curNode->_data)){parent = curNode;curNode = curNode->_left;}else if (kot(data) > kot(curNode->_data)){parent = curNode;curNode = curNode->_right;}// 搜索树中不允许有重复的值 对于已有值,不插入else{//return false;return make_pair(iterator(curNode), false);}}// while 循环结束后,代表找到了可以插入的位置// 找到位置了,但父节点不知道 新结点比自己大还是比自己小curNode = new Node(data);// 保存 cur 指针Node* newNode = curNode;if (kot(curNode->_data) < kot(parent->_data))parent->_left = curNode;elseparent->_right = curNode;curNode->_parent = parent;// 以上是二叉搜索树的插入逻辑,这样插入可能导致树不平衡,从而导致查找效率退化为 O(n)// 以下是红黑树的性质控制,是对二叉搜索树 进行的 控制平衡 操作// 控制近似平衡 ... // while 循环控制 颜色继续往上更新// 新插入的结点为红色 因此 parent 存在且 parent 为红色时,才需要更新颜色// while (parent && parent->_col == Red){Node* grandFather = parent->_parent;// parent 在 grandFather 左的场景if (parent == grandFather->_left){Node* uncle = grandFather->_right;// 情况一 cur为红 parent为红 grandFather为黑,uncle 存在且为红if (uncle && uncle->_col == Red){// p 和 u 变黑,g 变红,cur 变成 grandFather 继续往上更新// 变色parent->_col = uncle->_col = Black;grandFather->_col = Red;// 继续向上处理curNode = grandFather;parent = curNode->_parent;// 如果更新完后 grandFather 为红色:// 1. g 为根节点,那么parent为空// 2. g 上面还有结点 // 如果是黑色的,无需处理,进不去循环// 如果是红色,继续处理}// u不存在 或 u存在且为黑else{// 左左 -> 右单旋if (curNode == parent->_left){// g// p// cRotateR(grandFather);parent->_col = Black;grandFather->_col = Red;}// 左右 -> 左右双旋else{// g// p// cRotateL(parent);RotateR(grandFather);curNode->_col = Black;grandFather->_col = Red;}break;}}// parent 在 grandFather 右的场景else{Node* uncle = grandFather->_left;// 情况一 cur为红 parent为红 grandFather为黑,uncle 存在且为红if (uncle && uncle->_col == Red){// p 和 u 变黑,g 变红,cur 变成 grandFather 继续往上更新// 变色parent->_col = uncle->_col = Black;grandFather->_col = Red;// 继续向上处理curNode = grandFather;parent = curNode->_parent;// 如果更新完后 grandFather 为红色:// 1. g 为根节点,那么parent为空// 2. g 上面还有结点 // 如果是黑色的,无需处理,进不去循环// 如果是红色,继续处理}// u不存在 或 u存在且为黑else{// 右右 -> 左单旋if (curNode == parent->_right){// g// p// cRotateL(grandFather);parent->_col = Black;grandFather->_col = Red;}// 右左 -> 右左双旋else{// g// p// cRotateR(parent);RotateL(grandFather);curNode->_col = Black;grandFather->_col = Red;}break;}}}// 继续向上处理 parent = curNode->_parent 如果 parent == nullptr 会结束循环// parent == nullptr 结束循环时,curNode为_root结点,需要对 _root 节点变色_root->_col = Black; // 粗暴的处理,直接将根节点设为黑色,根节点为黑色总是正确的//return true;return make_pair(iterator(newNode), true);
}
4. map中 operator[] 的实现
- map 中的 insert 实现
// map 中的 iterator 是对 红黑树中 普通 iterator 的封装
pair<iterator, bool> insert(const pair<const K, V>& kv)
{return _tree.insert(kv);
}
- map 中的 operator[] 实现
V& operator[](const K& key)
{pair<iterator, bool> ret = insert(make_pair(key, V()));return (ret.first)->second;
}
operator[]的实现:
- 先根据
Key和Value插入pair,再返回pair中Value的引用
4. set 中 insert 的实现
实现 insert
- 由于
set中 只有const_iterator,因此修改了树的insert函数后,需要将 set 中 insert 函数的返回值和红黑树中 insert 的返回值做对应
// m_set::
pair<iterator, bool> insert(const K& key)
{// 这里树的 insert 返回的 iterator 是树里 的 普通iterator// 但是 set 中 insert 的返回值 pair 中的迭代器 是 const_iterator// 解决方法,先接收 树里的 普通迭代器,再用 普通迭代器构造 const_迭代器 返回pair<typename RBTree<K, K, SetKeyOfT>::iterator, bool> retPair = _tree.insert(key);// 下面用 普通迭代器去构造 const 迭代器 并 返回return pair<const_iterator, bool>(retPair.first, retPair.second);
}
由树的普通迭代器构造 const 迭代器 详解
// __TreeIterator::
typedef __TreeIterator<T, Ptr, Ref> Self;
typedef __TreeIterator<T, T*, T&> Iterator;// 普通迭代器构造 const 迭代器的函数
__TreeIterator(const Iterator& it):_node(it._node)
{ }
typedef __TreeIterator<T, T*, T&> Iterator:不论__TreeIterator被实例化成什么类型,Iterator始终为普通迭代器类型。函数__TreeIterator(const Iterator& it)的形参为普通迭代器的 const 引用- 当
__TreeIterator被实例化成普通迭代器时,此时该函数__TreeIterator(const Iterator& it)充当__TreeIterator类的拷贝构造函数 - 当
__TreeIterator被实例化成const迭代器时,此时该函数__TreeIterator(const Iterator& it)充当__TreeIterator类的构造函数,可以由普通迭代器构造 const 迭代器
- 当
七、完整代码实现
m_set
#pragma once#include "Red_Black_Tree.h"namespace m_set
{template<class K>class set{// 萃取器struct SetKeyOfT{const K& operator()(const K& key) const{return key;}};RBTree<K, K, SetKeyOfT> _tree;public://typedef typename RBTree<K, K, SetKeyOfT>::iterator iterator;// set 不允许修改,解决方案: set 的迭代器和const迭代器均为 const_iteratortypedef typename RBTree<K, K, SetKeyOfT>::const_iterator iterator;typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;// 库里的写法,set 中,这里的 iterator 实际是 const_iterator/*iterator begin() const{return _tree.begin();}iterator end() const{return _tree.end();}*/// set 的 iterator 都是 const_iterator,因此可以只提供 const 版本的迭代器const_iterator begin() const{return _tree.begin();}const_iterator end() const{return _tree.end();}// set // pair<iterator, bool> insert(const K& key){//return _tree.insert(key); //这里树的 insert 返回的 iterator 是树里 的 普通iterator// 但是 set 的 insert 的返回值 pair中的迭代器 是 const_iterator// 解决方法,先接收 树里的 普通迭代器,再用 普通迭代器构造 const_迭代器pair<typename RBTree<K, K, SetKeyOfT>::iterator, bool> ret = _tree.insert(key);// 下面用 普通迭代器去初始化 const 迭代器return pair<const_iterator, bool>(ret.first, ret.second);}};
}
m_map
#pragma once#include "Red_Black_Tree.h"namespace m_map
{template<class K, class V>class map{private:// 萃取器 把 T 对象中的 key 取出来struct MapKeyOfT{const K& operator()(const pair<const K, V>& kv) const{return kv.first;}};// map 要求实现只能修改 key 不能修改 value// 这样定义,pair 可以修改, pair 中的 K 不能修改RBTree<K, pair<const K, V>, MapKeyOfT> _tree;public:// 按照 set 实现 const 迭代器的方法实现的话,会导致 map 的 value 也不能修改/*typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::const_iterator iterator;typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::const_iterator const_iterator;*/// key 不能修改, value 能修改typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::iterator iterator;typedef typename RBTree<K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;iterator begin(){return _tree.begin();}iterator end(){return _tree.end();}const_iterator begin() const{return _tree.begin();}const_iterator end() const{return _tree.end();}V& operator[](const K& key){pair<iterator, bool> ret = insert(make_pair(key, V()));return (ret.first)->second;}std::pair<iterator, bool> insert(const pair<const K, V>& kv){return _tree.insert(kv);}};
}
改造后的红黑树
#pragma once
#include <assert.h>
#include <iostream>
#include <utility>
using namespace std;enum Colour {Red,Black
};// 红黑树被实例化 出两份,一份 _data 是 K 类型,一份_data 是 pair<K, V> 类型
template<class T>
struct RBTreeNode
{RBTreeNode<T>* _left;RBTreeNode<T>* _right;RBTreeNode<T>* _parent;T _data;Colour _col;// 新结点默认是红色的RBTreeNode(const T& data):_left(nullptr),_right(nullptr),_parent(nullptr),_data(data),_col(Red){ }
};template<class T, class Ptr, class Ref>
struct __TreeIterator
{typedef RBTreeNode<T> Node;typedef __TreeIterator<T, Ptr, Ref> Self;typedef __TreeIterator<T, T*, T&> Iterator;// 类模板 __TreeIterator 被实例化成 普通迭代器时, Self 和 Iterator 是同一个类型// 类模板 __TreeIterator 被实例化成 const迭代器时:// Self 就成了 const 迭代器,Iterator 始终是 普通迭代器Node* _node;__TreeIterator(Node* node):_node(node){ }__TreeIterator(const Iterator& it):_node(it._node){ }// const 迭代器相当于套了一层模板Ref operator*() const{if (_node == nullptr)assert(false);return _node->_data;}Ptr operator->() const{if (_node == nullptr)assert(false);return &(_node->_data);}bool operator!=(const Self& s) const { return _node != s._node; }bool operator==(const Self& s) const { return _node == s._node; }Self operator--(int){Self tmp(*this);--*this;return tmp;}Self& operator--(){// -- 就是 ++ 反过来// 左子树不为空,就去找左树的最大结点if (_node->_left){Node* subRight = _node->_left;while (subRight->_right)subRight = subRight->_right;_node = subRight;}else{// 孩子是父亲的右的那个节点Node* cur = _node;Node* parent = cur->_parent;while (parent && cur == parent->_left){cur = cur->_parent;parent = parent->_parent;}_node = parent;}return *this;}Self& operator++(){// 右树不为空,就访问右树的最左结点if (_node->_right){// 找右树的最左节点(最小节点)Node* subLeft = _node->_right;while (subLeft->_left)subLeft = subLeft->_left;_node = subLeft;}// 右树为空时分两种情况讨论else{Node* cur = _node;Node* parent = cur->_parent;// 找孩子是父亲左的那个祖先节点,就是下一个要访问的节点while (parent && cur == parent->_right){cur = cur->_parent;parent = parent->_parent;}// 有两种情况会结束循环 // 1. cur == parent->_left 时, 下一个访问的就是cur 的 父亲,_node = parent// 2. parent 为 空 时,cur 此时为根节点, 且 cur 的右为空,此时中序遍历结束_node = parent;}return *this;}Self operator++(int){Self tmp(*this);++*this;return tmp;}
};// 迭代器与const迭代器 是同一个类模板,传入不同的模板参数,实例化出不同的类型
// KeyOfT 是一个仿函数
// Set->RBTree<K,K,SetKeyOfT> _t
// map->RBTree<K,pair<K,V>, MapKeyOfT> _t;
template<class K, class T, class KeyOfT>
class RBTree
{
//public:
// int _rotateCount = 0;
private:typedef RBTreeNode<T> Node;RBTreeNode<T>* _root = nullptr;public:// 同一个类模板 传不同的参数,实例化出不同的类型typedef __TreeIterator<T, T*, T&> iterator;typedef __TreeIterator<T, const T*, const T&> const_iterator;// begin 返回的是 中序遍历的第一个位置iterator begin(){Node* leftMin = _root;while (leftMin && leftMin->_left)leftMin = leftMin->_left;return iterator(leftMin);}iterator end(){return iterator(nullptr);}// const 版本const_iterator begin() const{Node* leftMin = _root;while (leftMin && leftMin->_left){leftMin = leftMin->_left;}return const_iterator(leftMin);}const_iterator end() const{return const_iterator(nullptr);}public:// find 函数也需要用 KeyOfT 控制Node* find(const K& key) const{Node* curNode = _root;KeyOfT kot;while (curNode){if (key > kot(curNode->_data))curNode = curNode->_right;else if (key < kot(curNode->_data))curNode = curNode->_left;elsereturn curNode;}return nullptr;}std::pair<iterator, bool> insert(const T& data){// 先走二叉搜索树的插入逻辑if (_root == nullptr){_root = new Node(data);_root->_col = Black; // 性质 根节点是黑色的//return true;return make_pair(iterator(_root), true);}// _root 不为空时,二叉搜索树的逻辑Node* parent = nullptr;Node* curNode = _root;// 先找空,找到一个可以插入的位置// 使用 萃取器 取出 Key 设计相当巧妙// 标准库中的 pair 不能直接比较大小,我们设计了 一个萃取器 用于取出 T 中的 Key// 可以认为这里是 set 迁就了 map ,因为set的 K 可以直接比较,map 的pair<K, V> 需要取出 Key// 这是 map 和 set 的设计,_data 可能是 K,也可能是 pair// 如果是 K,可以直接比较,如果是map,标准库中的 pair<K, V> 的比较对象不支持仅比较K// 因子设计了仿函数 KeyOfT 如果是 set 返回 K, 如果是map 中的 pair<K, V>,取出 K 进行比较// KeyOfT 完全是为了 map 设计的,KeyOfT kot;while (curNode){if (kot(data) < kot(curNode->_data)){parent = curNode;curNode = curNode->_left;}else if (kot(data) > kot(curNode->_data)){parent = curNode;curNode = curNode->_right;}// 搜索树中不允许有重复的值 对于已有值,不插入else{//return false;return make_pair(iterator(curNode), false);}}// while 循环结束后,代表找到了可以插入的位置// 找到位置了,但父节点不知道 新结点比自己大还是比自己小curNode = new Node(data);// 保存 cur 指针Node* newNode = curNode;if (kot(curNode->_data) < kot(parent->_data))parent->_left = curNode;elseparent->_right = curNode;curNode->_parent = parent;// 以上是二叉搜索树的插入逻辑,这样插入可能导致树不平衡,从而导致查找效率退化为 O(n)// 以下是红黑树的性质控制,是对二叉搜索树 进行的 控制平衡 操作// 控制近似平衡 ... // while 循环控制 颜色继续往上更新// 新插入的结点为红色 因此 parent 存在且 parent 为红色时,才需要更新颜色// while (parent && parent->_col == Red){Node* grandFather = parent->_parent;// parent 在 grandFather 左的场景if (parent == grandFather->_left){Node* uncle = grandFather->_right;// 情况一 cur为红 parent为红 grandFather为黑,uncle 存在且为红if (uncle && uncle->_col == Red){// p 和 u 变黑,g 变红,cur 变成 grandFather 继续往上更新// 变色parent->_col = uncle->_col = Black;grandFather->_col = Red;// 继续向上处理curNode = grandFather;parent = curNode->_parent;// 如果更新完后 grandFather 为红色:// 1. g 为根节点,那么parent为空// 2. g 上面还有结点 // 如果是黑色的,无需处理,进不去循环// 如果是红色,继续处理}// u不存在 或 u存在且为黑else{// 左左 -> 右单旋if (curNode == parent->_left){// g// p// cRotateR(grandFather);parent->_col = Black;grandFather->_col = Red;}// 左右 -> 左右双旋else{// g// p// cRotateL(parent);RotateR(grandFather);curNode->_col = Black;grandFather->_col = Red;}break;}}// parent 在 grandFather 右的场景else{Node* uncle = grandFather->_left;// 情况一 cur为红 parent为红 grandFather为黑,uncle 存在且为红if (uncle && uncle->_col == Red){// p 和 u 变黑,g 变红,cur 变成 grandFather 继续往上更新// 变色parent->_col = uncle->_col = Black;grandFather->_col = Red;// 继续向上处理curNode = grandFather;parent = curNode->_parent;// 如果更新完后 grandFather 为红色:// 1. g 为根节点,那么parent为空// 2. g 上面还有结点 // 如果是黑色的,无需处理,进不去循环// 如果是红色,继续处理}// u不存在 或 u存在且为黑else{// 右右 -> 左单旋if (curNode == parent->_right){// g// p// cRotateL(grandFather);parent->_col = Black;grandFather->_col = Red;}// 右左 -> 右左双旋else{// g// p// cRotateR(parent);RotateL(grandFather);curNode->_col = Black;grandFather->_col = Red;}break;}}}// 继续向上处理 parent = curNode->_parent 如果 parent == nullptr 会结束循环// parent == nullptr 结束循环时,curNode为_root结点,需要对 _root 节点变色_root->_col = Black; // 粗暴的处理,直接将根节点设为黑色,根节点为黑色总是正确的//return true;return make_pair(iterator(newNode), true);}bool CheckColour(Node* root, int blacknum, int benchmark){if (root == nullptr){if (blacknum != benchmark)return false;return true;}// 检查黑色节点的数量if (root->_col == Black)++blacknum;// 检查有没有连续的红色结点if (root->_col == Red && root->_parent && root->_parent->_col == Red){cout << root->_kv.first << "出现连续红色节点" << endl;return false;}return CheckColour(root->_left, blacknum, benchmark)&& CheckColour(root->_right, blacknum, benchmark);}bool isBalance(){return _isBalance(_root);}bool _isBalance(Node* root = _root){if (root == nullptr)return true;if (root->_col != Black)return false;// 基准值int benchmark = 0;Node* cur = _root;while (cur){if (cur->_col == Black)++benchmark;cur = cur->_left;}return CheckColour(root, 0, benchmark);}int Height(){return _Height(_root);}int _Height(Node* root = _root){if (root == nullptr)return 0;int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;}private:// 旋转操作 可参考 红黑树博客
};
八、结语
至此,我们完成了一个 STL 风格的 map 和 set 容器封装。
通过模板与萃取器机制,我们让一棵红黑树同时支持不同类型的数据结构,并实现了 insert、operator[]、迭代器等关键功能。
这个过程不仅帮助我们理解了 STL 的底层逻辑,也展示了泛型编程的强大之处——
同一套代码,通过不同的模板参数,就能支撑多种容器行为。
以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步
分享到此结束啦
一键三连,好运连连!你的每一次互动,都是对作者最大的鼓励!
征程尚未结束,让我们在广阔的世界里继续前行!🚀
