当前位置: 首页 > news >正文

【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 风格的 mapset 容器。

STL 中的 mapset 虽然都基于红黑树实现,但两者的数据结构并不相同:

  • set 只保存键 key
  • map 保存键值对 pair<const K, V>

为了兼容两种场景,标准库采用了泛型模板和萃取器 (KeyOfT) 的设计思想,使红黑树在编译期即可灵活适配不同容器。
本文将带你一步步封装出一个可运行的 mapset,深入理解它们与红黑树之间的联系,以及 STL 容器设计的核心思路。


一、改造红黑树

1. 分析源码

set 与 map 的底层都是红黑树,但是我们不能直接使用一棵普通的红黑树套进去,因为 set 和 map 中所存储的数据类型是不一样的。

  • set 中是单个值 key
  • map 中是一个 pair 类型。那么我们应该如何解决呢?我们来参考一下 STL 库中的写法。

SGI-STL30 版本源代码,map 和 set 相关的实现代码分布在 mapsetstl_map.hstl_set.hstl_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>中,Valuepair<const int, string>)
    • 红黑树节点存储完整的键值对pair,从而支持“按 key 关联 value ”的搜索逻辑

在这里插入图片描述

注意事项:关于 value_type 的特殊含义

  • 源码里的模板参数常用 T 代表“节点存储的数据类型(即这里的Value
  • rb_tree 内部定义的value_type,并非我们日常说的“key/value 里的value”,而是红黑树节点实际存储的数据的类型(对setkey 类型,对 mappair<const Key,T> 类型)

为什么红黑树需要两个模板参数(Key & Value)?

既然 rb_tree 第二个模板参数 Value已经控制了红黑树节点中存储的数据类型,为什么还要传第一个模板参数 Key 呢?

尤其是 set,两个模板参数均为K,这是为什么呢?

核心原因在于 find/erase 等操作的参数需求

  • 对 set 和 map 来说find /erase函数的参数是 Key 类型(按key 查找、删除),而非完整的节点数据(Value 类型)

  • set而言

    • KeyValue 类型相同(节点存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预设的比较规则进行比较
  • setKey可以完成预期比较,而 std::pair 的默认支持的是 keyvalue 一起参与比较,不符合我们仅比较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 的设计相当巧妙

  • setmap 的设计,_data 分别是 Keypair
  • 如果是 set 中的Key, 可以直接比较,如果是map,标准库中的 pair<K, V> 的比较不支持仅比较K

因此设计了仿函数萃取器 KeyOfT ,用于取出 T 中的 Key 来进行比较

  • 如果是 set 返回 K,如果是 map 中的 pair<K, V>,取出 K 进行比较
  • 可以认为这里是 set 迁就了 map ,因为 set 的 K 本身就可以直接比较,mappair<K, V> 需要借助KeyOfT取出 Key进行比较

KeyOfT完全是为了map设计的setSetKeyOfT 仅仅是为了保持统一的接口风格


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 的实现思路与 listiterator 框架一致:

  • 用一个类封装 “结点指针”
  • 再通过重载运算符,让迭代器能像指针一样完成访问和移动行为(如:*it++itit-> 等行为)

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 的实现思路与 listiterator 实现思路一致,模板参数的控制思路也一致
  • 构造函数__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 迭代器 的拷贝构造

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 版本的 beginend 函数,普通对象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;即可实现mapkey 不能修改, 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 等函数
}
  • 实现mapkey 不能修改, value 能修改是在pair存储层增加了const来解决这个问题,因此beginend常规实现即可

    • 普通 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

begin与end函数

  • mapKey 不能修改是在 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行为的哲学

mapoperator[]的行为等效如下:

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[]的实现:

  • 先根据 KeyValue 插入pair,再返回pairValue的引用

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 风格的 mapset 容器封装。
通过模板与萃取器机制,我们让一棵红黑树同时支持不同类型的数据结构,并实现了 insertoperator[]、迭代器等关键功能。

这个过程不仅帮助我们理解了 STL 的底层逻辑,也展示了泛型编程的强大之处——
同一套代码,通过不同的模板参数,就能支撑多种容器行为。


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!

你的每一次互动,都是对作者最大的鼓励!


征程尚未结束,让我们在广阔的世界里继续前行! 🚀

http://www.dtcms.com/a/569122.html

相关文章:

  • sscanf解析
  • 中专生实习找什么工作?
  • 车辆车身颜色特征识别
  • 【JUnit实战3_28】第十七章:用 JUnit 5 实测 SpringBoot 项目
  • 摆脱局域网束缚!用 DS File+cpolar 让 NAS 文件随用随取
  • 网站制作费电力行业做的好的招投标网站
  • 百度网址大全网站大全石家庄网页设计培训班
  • 设计模式学习(十二)状态模式
  • shell-基于k8s/docker管理容器、监控模型训练所消耗的最大CPU与最大内存脚本
  • Maven打包时指定输出路径、以时间戳命名包名和路径名,结合IDEA以指令脚本方式动态配置输出目录
  • PortSwigger
  • Doxygen入门指南:从注释到自动文档
  • Docker 部署 Elasticsearch 8.12 + Kibana + Nginx 负载均衡
  • yolo 训练 动态改变类别
  • SQL大表关联优化全攻略
  • 第五章:构建用户界面(UMG) - 游戏内HUD
  • CSS 雪碧图和 SVG 雪碧图的原理和区别
  • 网站底部代码特效邢台网红隧道
  • 网站降权表现营销型公司网站有哪些
  • 评估指标+数据不匹配+贝叶斯最优误差(分析方差和偏差)+迁移学习+多任务学习+端到端深度学习
  • 外国食品优秀设计网站做网站电话销售
  • 构建下一代法律智能助手:需求分析、资源整合与系统设计
  • Oracle AWR案例分析:精准定位SQL执行计划切换的时间点
  • 2025年7月一区SCI优化算法-Logistic-Gauss Circle optimizer-附Matlab免费代码
  • abpVnext 获取token报错,配置文件从sqlerver切换到oracle,再切换回来sqlerver无法获取token
  • 成都哪里有做网站的公司wordpress在本地搭建
  • C++进阶:(四)set系列容器的全面指南
  • 【Java零碎知识点】----- java.util.Random 与 Math.random()
  • 补充内容:YOLOv5损失函数解析+代码阅读
  • 北仑网站建设培训学校游戏开发需要什么学历