【C++】封装红黑树实现map和set容器(详解)

封装红黑树实现map和set容器

封装红黑树实现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 的实现结构框架核心部分截取出来如下

cpp 复制代码
// 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 tree
	link_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. 改造实现代码整体框架

结点颜色的枚举类

  • 我们定义一个枚举类,枚举值表示结点的颜色
cpp 复制代码
enum Colour {
	Red,
	Black
};

红黑树的结点定义

  • 红黑树结点为模版实现
cpp 复制代码
// 由于 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类访问成员


红黑树架构设计

cpp 复制代码
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值
cpp 复制代码
template<class K, class T, class KeyOfT>
class RBTree
{
private:
	typedef RBTreeNode<T> Node;
	RBTreeNode<T>* _root = nullptr;
public:
    // ...迭代器和相关成员函数 实现
}

SetKeyOfT

cpp 复制代码
// 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

cpp 复制代码
// 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为定义出的仿函数对象
cpp 复制代码
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;
		}
		// 搜索树中不允许有重复的值  对于已有值,不插入
		else
			return false;
	}
	// while 循环结束后,代表找到了可以插入的位置
	// 找到位置了,但父节点不知道 新结点比自己大还是比自己小
	curNode = new Node(data);
	if (kot(curNode->_data) < kot(parent->_data))
		parent->_left = curNode;
	else
		parent->_right = curNode;
	
	curNode->_parent = parent;
    // ... 仅以上代码 有部分修改,其余代码和红黑树的插入一致
}

三、迭代器与相关重载实现

这里的 iterator 的实现思路与 listiterator 框架一致:

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

1. 自定义树的iterator类

cpp 复制代码
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 迭代器

cpp 复制代码
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*

cpp 复制代码
Ref operator*() const
{
	if (_node == nullptr)
		assert(false);
	return _node->_data;
}
  • 防止访问空结点 :访问空结点时assert(false)
  • *it需要访问到结点中的数据,引用返回,因此返回结点中的数据return _node->_data

3. operator->

cpp 复制代码
Ptr operator->() const
{
	if (_node == nullptr)
		assert(false);
	return &(_node->_data);
}
  • 防止访问空结点 :访问空结点时assert(false)
  • it->通过指针访问数据的行为 ,因此返回结点中的地址return &(_node->_data)

4. operator!=

cpp 复制代码
bool operator!=(const Self& s) const 
{ 
	return _node != s._node; 
}

5. operator==

cpp 复制代码
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

  • 前置++
cpp 复制代码
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;
}

  • 后置++
cpp 复制代码
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

  • 前置--
cpp 复制代码
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;
}

  • 后置--
cpp 复制代码
Self operator--(int)
{
	Self tmp(*this);
	--*this;
	return tmp;
}

四、红黑树的iterator以及const_itreator的封装

1. 封装iterator与const_iterator

在树的层面实现迭代器:

cpp 复制代码
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

实现如下

cpp 复制代码
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

cpp 复制代码
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 中源码中的实现:截取部分如下

cpp 复制代码
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迭代器实现普通迭代器,我们仿照其实现即可
cpp 复制代码
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
cpp 复制代码
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 不支持修改

cpp 复制代码
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 迭代器 的适当封装
cpp 复制代码
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对象版本
cpp 复制代码
// 普通 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[]的行为等效如下:

cpp 复制代码
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 语句进行了修改
cpp 复制代码
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;
	else
		parent->_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
					// c
					RotateR(grandFather);
					parent->_col = Black;
					grandFather->_col = Red;
				}
				// 左右 ->  左右双旋
				else
				{
					//     g
					//   p
					//      c
					RotateL(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
					//      c
					RotateL(grandFather);
					parent->_col = Black;
					grandFather->_col = Red;
				}
				// 右左 ->  右左双旋
				else
				{
					//  g
					//    p
					//  c
					RotateR(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 实现
cpp 复制代码
// map 中的 iterator 是对 红黑树中 普通 iterator 的封装
pair<iterator, bool> insert(const pair<const K, V>& kv)
{
    return _tree.insert(kv);
}
  • map 中的 operator[] 实现
cpp 复制代码
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 的返回值做对应
cpp 复制代码
// 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 迭代器 详解

cpp 复制代码
// __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

cpp 复制代码
#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_iterator
		typedef 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

cpp 复制代码
#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);
		}
	};
}

改造后的红黑树

cpp 复制代码
#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;
			else
				return 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;
		else
			parent->_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
						// c
						RotateR(grandFather);
						parent->_col = Black;
						grandFather->_col = Red;
					}
					// 左右 ->  左右双旋
					else
					{
						//     g
						//   p
						//      c
						RotateL(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
						//      c
						RotateL(grandFather);
						parent->_col = Black;
						grandFather->_col = Red;
					}
					// 右左 ->  右左双旋
					else
					{
						//  g
						//    p
						//  c
						RotateR(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 的底层逻辑,也展示了泛型编程的强大之处------
同一套代码,通过不同的模板参数,就能支撑多种容器行为。


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

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

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


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

相关推荐
Laity______2 小时前
指针(2)
c语言·开发语言·数据结构·算法
是苏浙2 小时前
零基础入门C语言之C语言实现数据结构之顺序表经典算法
c语言·开发语言·数据结构·算法
大锦终2 小时前
【Linux】网络层与数据链路层中重点介绍
linux·运维·服务器·网络
lht6319356123 小时前
从Windows通过XRDP远程访问和控制银河麒麟 v10服务器
linux·运维·服务器·windows
qiudaorendao3 小时前
作业11.9
linux·服务器·apache
wxin_VXbishe4 小时前
springboot在线课堂教学辅助系统-计算机毕业设计源码07741
java·c++·spring boot·python·spring·django·php
夕泠爱吃糖4 小时前
template关键字
开发语言·c++·template
mit6.8244 小时前
[Avoid-MPC] AvoidanceStateMachine | `Step`心跳函数 | Callback设计
c++
ceclar1234 小时前
C++文件操作
开发语言·c++