【C++】封装红黑树实现mymap和myset

前文回顾:【C++】红黑树:使用及实现

目录

[一 源码及框架分析](#一 源码及框架分析)

[二 模拟实现map和set](#二 模拟实现map和set)

[1 实现步骤](#1 实现步骤)

[2 实现出复用红⿊树的框架,并支持insert](#2 实现出复用红⿊树的框架,并支持insert)

[3 支持iterator实现](#3 支持iterator实现)

(1)实现思路分析实现思路分析)

(2)实现代码

[4 const_iterator](#4 const_iterator)

[5 key不支持修改的问题](#5 key不支持修改的问题)

[6 operator [ ]](#6 operator [ ])


一 源码及框架分析

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

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

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

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

  • 吐槽一下,这里源码命名风格比较乱,set模板参数用的Key命名,map用的是Key和T命名,而rb_tree用的又是Key和Value,可见大佬有时写代码也不规范,乱弹琴。

把下面的树写成一个泛型,写成一个模板,模板的值叫做valus_field,模板的值由上层决定(class value)

keyOfT是仿函数,作用是将class T的key值取出来(因为不确定T是什么类型,T的类型由上层决定

我们可以在内部实现一个keyOfT,仿函数会去调用对应的operator ()

在map里面:

在set内部:

data的类型有可能是key,也有可能是pair


二 模拟实现map和set

1 实现步骤

1、实现红⿊树
2、封装map和set框架,解决KeyOfT
3、iterator
4、const_iterator
5、key不⽀持修改的问题
6、operator[]

2 实现出复用红⿊树的框架,并支持insert

• 参考源码框架,map和set复用之前我们实现的红黑树。

• 我们这里相比源码调整一下,key参数就用K,value参数就用V,红黑树中的数据类型,我们使用T。

• 其次因为RBTree实现了泛型不知道T参数到底是K,还是pair<K, V>,那么insert内部进行插入逻辑比较时,就没办法进行比较,因为pair的默认支持的是key和value一起参与比较,而我们需要的是任何时候只比较key,所以我们在map和set层分别实现一个MapKeyOfT和SetKeyOfT的仿函数传给RBTree的KeyOfT,然后RBTree中通过KeyOfT仿函数取出T类型对象中的key,再进行比较,具体细节参考如下代码实现

cpp 复制代码
// 源码中pair⽀持的<重载实现 
template <class T1, class T2>
bool operator< (const pair<T1,T2>& lhs, const pair<T1,T2>& rhs)
{ return lhs.first<rhs.first || (!(rhs.first<lhs.first) && 
lhs.second<rhs.second); }

Mymap.h

cpp 复制代码
// Mymap.h
namespace bit
{
 template<class K, class V>
 class map
 {
 struct MapKeyOfT
 {
 const K& operator()(const pair<K, V>& kv)
 {
 return kv.first;
 }
 };
 public:
 bool insert(const pair<K, V>& kv)
 {
 return _t.Insert(kv);
 }
 private:
 RBTree<K, pair<K, V>, MapKeyOfT> _t;
 };
}

Myset.h

cpp 复制代码
// Myset.h
namespace bit
{
 template<class K>
 class set
 {
 struct SetKeyOfT
 {
 const K& operator()(const K& key)
 {
 return key;
 }
 };
 public: 
 bool insert(const K& key)
 {
 return _t.Insert(key);
 } 
 private:
 RBTree<K, K, SetKeyOfT> _t;
 };
}

RBTree.h

cpp 复制代码
// RBTree.h
enum Colour
{
 RED,
 BLACK
};

template<class T>
struct RBTreeNode
{
 T _data;
 RBTreeNode<T>* _left;
 RBTreeNode<T>* _right;
 RBTreeNode<T>* _parent;
 Colour _col;
 RBTreeNode(const T& data)
 : _data(data)
 , _left(nullptr)
 , _right(nullptr)
 , _parent(nullptr)
 {}
};

// 实现步骤: 
// 1、实现红⿊树 
// 2、封装map和set框架,解决KeyOfT 
// 3、iterator 
// 4、const_iterator 
// 5、key不⽀持修改的问题 
// 6、operator[] 

template<class K, class T, class KeyOfT>
class RBTree
{
private:
 typedef RBTreeNode<T> Node;
 Node* _root = nullptr;
 
public:
 bool Insert(const T& data)
 {
 if (_root == nullptr)
 {
 _root = new Node(data);
 _root->_col = BLACK;
 return true;
 }
 KeyOfT kot;
 Node* parent = nullptr;
 Node* cur = _root;
 while (cur)
 {
 if (kot(cur->_data) < kot(data))
 {
 parent = cur;
 cur = cur->_right;
 }
 else if (kot(cur->_data) > kot(data))
 {
 parent = cur;
 cur = cur->_left;
 }
 else
 {
 return false;
 }
 }
 cur = new Node(data);
 Node* newnode = cur;
 // 新增结点。颜⾊给红⾊ 
 cur->_col = RED;
if (kot(parent->_data) < kot(data))
 {
 parent->_right = cur;
 }
 else
 {
 parent->_left = cur;
 }
 cur->_parent = parent;
 //...
 return true;
 }
}

3 支持iterator实现

(1)实现思路分析

iterator实现的大框架跟list的iterator思路是一致的,用一个类型封装结点的指针,再通过重载运算符实现,迭代器像指针一样访问的行为

• 这里的难点是operator++和operator-- 的实现。之前使用部分,我们分析了,map和set的迭代器走的是中序遍历,左子树->根结点->右子树,那么begin()会返回中序第一个结点的iterator也就是10所在结点的迭代器

• 迭代器++的核心逻辑就是不看全局,只看局部,只考虑当前中序局部要访问的下一个结点

迭代器++时,如果it指向的结点的右子树不为空,代表当前结点已经访问完了,要访问下一个结点是右子树的中序第一个,一棵树中序第一个是最左结点,所以直接找右子树的最左结点即可。

• 迭代器++时,如果it指向的结点的右子树空,代表当前结点已经访问完了且当前结点所在的子树也访问完了,要访问的下一个结点在当前结点的祖先里面,所以要沿着当前结点到根的祖先路径向上找。

• 如果当前结点是父亲的左,根据中序左子树->根结点->右子树,那么下一个访问的结点就是当前结点的父亲;如下图:it指向25,25右为空,25是30的左,所以下一个访问的结点就是30。

• 如果当前结点是父亲的右,根据中序左子树->根结点->右子树,当前当前结点所在的子树访问完了,当前结点所在父亲的子树也访问完了,那么下一个访问的需要继续往根的祖先中去找,直到找到孩子是父亲左的那个祖先就是中序要访问的下一个结点。如下图:it指向15,15右为空,15是10的右,15所在子树访问完了,10所在子树也访问完了,继续往上找,10是18的左,那么下一个访问的结点就是18

• end()如何表示呢?如下图:当it指向50时,++it时,50是40的右,40是30的右,30是18的右,18到根没有父亲,没有找到孩子是父亲左的那个祖先,这时父亲为空了,那我们就把it中的结点指针置为nullptr,我们用nullptr去充当end 。需要注意的是stl源码中,红黑树增加了一个哨兵位头结点做为end(),这哨兵位头结点和根互为父亲,左指向最左结点,右指向最右结点。相比我们用nullptr作为end(),差别不大,它能实现的,我们也能实现。只是**--end()判断到结点为空时,特殊处理一下,让迭代器结点指向最右结点**。具体参考迭代器--实现。 • 迭代器--的实现跟++的思路完全类似,逻辑正好反过来即可,因为它访问顺序是右子树->根结点->左子树,具体参考下面代码实现。

set的iterator也不支持修改,我们把set的第二个模板参数改成const K即可,RBTree<K, const K, SetKeyOfT> _t; • map的iterator不支持修改key但是可以修改value,我们把map的第二个模板参数pair的第一个参数改成const K即可,RBTree<K, pair<const K, V>, MapKeyOfT> _t; • 支持完整的迭代器还有很多细节需要修改,具体参考下面题的代码。

这里的最后一个图,是表示有时候会增加一个头节点来帮助实现寻找最左边的节点和最右边的节点,但是这个方法的实现思路稍微有点麻烦,实现出来的结果和我们现在所讲的方法是一样的,所以增加头节点的方法代码我们就不在这里实现了。

(2)实现代码
cpp 复制代码
// RBTree<K, pair<K, V>> _t;-> // map
 // RBTree<K, K> _t;->		   // set
 
 template<class T, class Ref, class Ptr>
 struct RBTreeIterator
 {
 	typedef RBTreeNode<T> Node;
 	typedef RBTreeIterator<T, Ref, Ptr> Self;
 
 	Node* _node;
 
 	RBTreeIterator(Node* node)
 		:_node(node)
 	{}
 
 	Ref operator*()
 	{
 		return _node->_data;
 	}
 
 	Ptr operator->()
 	{
 		return &_node->_data;
 	}
 	// 10:30
 	Self& operator++()
 	{
 		if (_node->_right)
 		{
 			Node* minRight = _node->_right;
 			while (minRight->_left)
 			{
 				minRight = minRight->_left;
 			}
 
 			_node = minRight;
 		}
 		else
 		{
 			Node* cur = _node;
 			Node* parent = cur->_parent;
 			while (parent && cur == parent->_right)
 			{
 				cur = parent;
 				parent = parent->_parent;
 			}
 
 			_node = parent;
 		}
 
 		return *this;
 	}
 
 	bool operator!=(const Self& s)
 	{
 		return _node != s._node;
 	}
 
 	bool operator==(const Self& s)
 	{
 		return _node == s._node;
 	}
cpp 复制代码
Iterator Find(const K& key)
 	{
 		KeyOfT kot;
 		Node* cur = _root;
 		while (cur)
 		{
 			if (kot(cur->_data) < key)
 			{
 				cur = cur->_right;
 			}
 			else if (kot(cur->_data) > key)
 			{
 				cur = cur->_left;
 			}
 			else
 			{
 				return Iterator(cur);
 			}
 		}

find不仅能实现查找,还能实现修改

4 const_iterator

在 map 和 set 中,const_iterator 的核心需求是 "只读访问"(不能通过迭代器修改容器元素),且需兼容红黑树的迭代器逻辑

cpp 复制代码
template<class K, class T, class KeyOfT>
 struct RBTree
 {
 	typedef RBTreeNode<T> Node;
 public:
 	typedef RBTreeIterator<T, T&, T*> Iterator;
 	typedef RBTreeIterator<T, const T&, const T*> ConstIterator;
cpp 复制代码
ConstIterator Begin() const
	{
		Node* minLeft = _root;
		while (minLeft && minLeft->_left)
		{
			minLeft = minLeft->_left;
		}

		return ConstIterator(minLeft);
	}

	ConstIterator End() const
	{
		return ConstIterator(nullptr);
	}

测试函数

cpp 复制代码
template<class T>
 void func(const bit::set<T>& s)
 {
 	typename bit::set<T>::const_iterator it = s.begin();
 	while (it != s.end())
 	{
 		//*it = 1;
 		cout << *it << " ";
 		++it;
 	}
 	cout << endl;
 }
  1. typename 的作用typename bit::set<T>::const_iterator 中,typename 必须加 ------ 因为 const_iterator 是依赖于模板参数 T 的嵌套类型,编译器无法默认判断其为 "类型",需用 typename 显式声明。

  2. 常量迭代器的限制 :注释掉的 *it = 1 是非法的,因为 const_iterator 是 "常量迭代器",仅支持读取元素(operator*() 返回 const T 或值类型),不支持修改 ------ 这与函数参数 const bit::set<T>& s 一致(保证容器不被修改)。

5 key不支持修改的问题

mapset 中,key 不能修改 是核心设计原则,本质是为了 保证容器的有序性(二者底层均基于红黑树 / 二叉搜索树),key 是排序的依据,修改 key 会破坏树的结构和有序性

map 和 set 的核心特性是「有序且不重复」

封装时的核心思路是 让 key 成为「只读类型」

在set.h中:

cpp 复制代码
namespace bit
 {
 	template<class K>
 	class set
 	{
 		struct SetKeyOfT
 		{
 			const K& operator()(const K& key)
 			{
 				return key;
 			}
 		};

在map.h中:

cpp 复制代码
template<class K, class V>
 	class map
 	{
 		struct MapKeyOfT
 		{
 			const K& operator()(const pair<K, V>& kv)
 			{
 				return kv.first;
 			}
 		};

6 operator [ ]

map 中,operator[] 是一个 非常便捷但容易踩坑 的核心接口,其核心功能是「访问或插入键值对」,且仅 map 支持set 无键值对结构,不提供 operator[]

map要⽀持 [ ] 主要需要修改insert返回值⽀持,修改RBtree中的insert返回值为

pair<Iterator, bool> Insert(const T& data)

cpp 复制代码
template<class K, class T, class KeyOfT>
 struct RBTree
 {
 	typedef RBTreeNode<T> Node;
 public:
 	typedef RBTreeIterator<T, T&, T*> Iterator;
 	typedef RBTreeIterator<T, const T&, const T*> ConstIterator;
 
 	~RBTree()
 	{
 		Destroy(_root);
 		_root = nullptr;
 	}
 
 	Iterator Begin()
 	{
 		//....
 	}
 
 	Iterator End()
 	{
 		return Iterator(nullptr);
 	}
 
 	ConstIterator Begin() const
 	{
 		//....
 	}
 
 	ConstIterator End() const
 	{
 		return ConstIterator(nullptr);
 	}
 
 	pair<Iterator, bool> Insert(const T& data)
 	{
 		if (_root == nullptr)
 		{
 			_root = new Node(data);
 			_root->_col = BLACK;
 			return {Iterator(_root), true};
 		}
 
 		KeyOfT kot;
 		Node* parent = nullptr;
 		Node* cur = _root;
 		while (cur)
 		{
 			if (kot(cur->_data) < kot(data))
 			{
 				parent = cur;
 				cur = cur->_right;
 			}
 			//else if (kot(cur->_data) > kot(data))
 			else if (kot(data) < kot(cur->_data))
 			{
 				parent = cur;
 				cur = cur->_left;
 			}
 			else
 			{
 				return { Iterator(cur), false };
 			}
 		}
 
 		// 新增红色
 		cur = new Node(data);
 		Node* newnode = cur;
 		cur->_col = RED;
 		if (kot(parent->_data) < kot(data))
 		{
 			parent->_right = cur;
 		}
 		else
 		{
 			parent->_left = cur;
 		}
 		cur->_parent = parent;
 
 		
 		while (parent && parent->_col == RED)
 		{
 			Node* grandfather = parent->_parent;
 			if (grandfather->_left == parent)
 			{
 				//    g
 				//  p   u
 				Node* uncle = grandfather->_right;
 				// 叔叔存在且为空
 				if (uncle && uncle->_col == RED)
 				{
 					// 变色+继续往上处理
 					parent->_col = uncle->_col = BLACK;
 					grandfather->_col = RED;
 					
 					cur = grandfather;
 					parent = cur->_parent;
 				}
 				else // 叔叔不存在或者叔叔存在且为黑
 				{
 					//    g
 					//  p   u
 					//c
 					// 单旋+变色
 					if (cur == parent->_left)
 					{
 						RotateR(grandfather);
 						parent->_col = BLACK;
 						grandfather->_col = RED;
 					}
 					else
 					{
 						//    g
 						//  p   u
 						//	  c
 						// 双旋+变色
 						RotateL(parent);
 						RotateR(grandfather);
 
 						cur->_col = BLACK;
 						grandfather->_col = RED;
 					}
 
 					break;
 				}
 			}
 			else 
 			{
 				//   g
 				// u   p
 				Node* uncle = grandfather->_left;
 				// 叔叔存在且为红,-》变色即可
 				if (uncle && uncle->_col == RED)
 				{
 					parent->_col = uncle->_col = BLACK;
 					grandfather->_col = RED;
 						// 继续往上处理
 						cur = grandfather;
 					parent = cur->_parent;
 				}
 				else
 				{
 					// 情况二:叔叔不存在或者存在且为黑
 					// 旋转+变色
 					//    g
 					//  u   p
 					// c
 					if (cur == parent->_right)
 					{
 						RotateL(grandfather);
 						parent->_col = BLACK;
 						grandfather->_col = RED;
 					}
 					else
 					{
 						//     g
 						//   u   p
 						// c
 						RotateR(parent);
 						RotateL(grandfather);
 						cur->_col = BLACK;
 						grandfather->_col = RED;
 					}
 					break;
 				}
 			}
 		}
 
 		_root->_col = BLACK;
 
 		return { Iterator(newnode), true };
 	}
相关推荐
云泽8085 小时前
C++ List 容器详解:迭代器失效、排序与高效操作
开发语言·c++·list
xlq223226 小时前
15.list(上)
数据结构·c++·list
云帆小二6 小时前
从开发语言出发如何选择学习考试系统
开发语言·学习
光泽雨6 小时前
python学习基础
开发语言·数据库·python
Elias不吃糖6 小时前
总结我的小项目里现在用到的Redis
c++·redis·学习
ANYOLY6 小时前
Sentinel 限流算法详解
算法·sentinel
AA陈超7 小时前
使用UnrealEngine引擎,实现鼠标点击移动
c++·笔记·学习·ue5·虚幻引擎
百***06017 小时前
python爬虫——爬取全年天气数据并做可视化分析
开发语言·爬虫·python
jghhh017 小时前
基于幅度的和差测角程序
开发语言·matlab
fruge7 小时前
自制浏览器插件:实现网页内容高亮、自动整理收藏夹功能
开发语言·前端·javascript