【 C++ 】用一颗红黑树封装map和set

目录

1、前言

2、红黑树模板参数的控制

3、模板参数中仿函数的增加

4、红黑树迭代器的实现

5、红黑树的begin()和end()

6、红黑树的Find查找函数

7、红黑树源码链接

8、set的代码

9、map的代码


1、前言

我们都知道set是K模型的容器,而map是KV模型的容器,但是它俩的底层都是用红黑树实现的,上篇博文中我们模拟实现了一颗红黑树,接下来将对其进行改造,继而用一颗红黑树完美的封装map和set。说白了map和set就是一个光杆司令,其内部的主要功能都是套用了红黑树现成的,只是稍作改动即可。


2、红黑树模板参数的控制

既然set是K模型,map是KV模型,正如stl库里的map和set,如图所示:

可我先前实现的红黑树是KV模型啊,难道说为了适配set模型再拷贝一份做修改?duck不必,只需要对模板参数进行修改即可,先看看库里的红黑树是如何针对模板参数进行解决的:

通过这里就能够很清晰的看出库里的节点的存储类型是根据set和map的第二个模板参数决定的,第二个参数存的是什么,节点存的就是什么类型,继而可以满足set和map的需求,而现在又可能引发一个新的问题:

  • 既然数据类型看第二个模板参数,那第一个模板参数有何用处? 

因为在红黑树中,无可避免会要求实现find等对K有需求的函数,因为find函数主要是通过Key进行查找的,如若省略第一个模板参数,那么map就无法进行find查找操作。

接下来,我们按照库里红黑树的样子对我们自己写的进行一个调整:

//节点类
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)
	{}
};
// 红黑树的类
// T决定红黑树中存储的什么数据
// set RBTree<K, K>
// map RBTree<K, pair<K, V>>
template <class K, class T>
class RBTree
{
	typedef RBTreeNode<T> Node;//T决定节点存储类型的数据
public:
    //……
private:
	Node* _root = nullptr;
};

对红黑树的模板参数修改好了,那么我map(KV模型)和set(K模型)自然而然就能够适配了:

  • set:
namespace cpp
{
	template<class K>
	class set
	{
    public:
        //……
	private:
		RBTree<K, K> _t;
	};
}
  • map:
namespace cpp
{
	template<class K, class V>
	class map
	{
    public:
        //……
	private:
		RBTree<K, pair<K, V>> _t;
	};
}

3、模板参数中仿函数的增加

由于现在红黑树的节点类型是T,当容器为set时,T就是键值Key,可以直接进行比较,当容器是map时,T就是pair<Key, Value>,此时不能直接比较,而需要从此键值对中取出Key,再拿Key进行大小比较。

为了解决这一点,我们可以在红黑树的模板参数上再加一层仿函数,此参数专门用于获得Key类型,而这个仿函数的实现是在map和set内部封装的,具体操作如下:

  • map:
template<class K, class V>
class map
{
    //仿函数
	struct MapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;//返回键值key
		}
	};
public:
    //……
private:
	RBTree<K, pair<K, V>, MapKeyOfT> _t;
};
  • set:
template<class K>
class set
{
    //仿函数
	struct SetKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};
public:
    //……
private:
	RBTree<K, K, SetKeyOfT> _t;
};

下面画图演示具体的调用情况:


4、红黑树迭代器的实现

红黑树的正向迭代器实际上就是对结点指针进行了封装,因此在正向迭代器当中实际上就只有一个成员变量,那就是正向迭代器所封装结点的指针。

//迭代器的类
template<class T, class Ref, class Ptr>
struct __RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef __RBTreeIterator<T, Ref, Ptr> Self;//把迭代器typedef,简化后续使用
    //……
	Node* _node;
};

而在其内部,我们要完成如下操作:

  • 1、构造函数
  • 2、*->运算符重载
  • 3、!===运算符重载
  • 4、++运算符重载
  • 5、--运算符重载

接下来具体展开演示:

  • 1、构造函数

构造函数我们直接通过一个节点的指针从而构造一个正向迭代器即可。

//构造函数
__RBTreeIterator(Node* node)
	:_node(node)
{}
  • 2、*->运算符重载

*运算符就是解引用,直接返回对应节点数据的引用即可。而->运算符返回的是对应节点数据的指针。

//*运算符重载
Ref operator*()
{
	return _node->_data;//返回_data数据本身
}
//->运算符重载
Ptr operator->()
{
	return &_node->_data;//返回_data数据的地址
}
  • 3、!===运算符重载

!=运算符直接返回两个节点是否不同,而==运算符直接返回两个节点是否相同即可。

//!=
bool operator!=(const Self& s)
{
	return _node != s._node;
}
//==
bool operator==(const Self& s)
{
	return _node == s._node;
}
  • 4、++运算符重载

++运算符又分前置++和后置++

  • 前置++:

首先,这里红黑树迭代器里的++后的值应该是按此位置开始往后中序遍历的下一个。而这个下一个节点的值理应比原先的大,想要找到这个位置,结合二叉搜索树的性质,理应在右子树当中去寻找,而这又要看右子树是否为空,具体操作如下:

  • 1、右子树非空:直接遍历找到右子树的最左节点即可
  • 2、右子树为空:找祖先里面,孩子是父亲左的那个祖先节点
  • 3、当parent遍历到空时,++结束
  • 4、注意前置++返回的是++后的值
//前置++
Self& operator++()
{
	if (_node->_right == nullptr)//右子树为空
	{
		//找祖先里面,孩子是父亲左的那个
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent && parent->_right == cur)
		{
			cur = cur->_parent;
			parent = parent->_parent;
		}
		_node = parent;
	}
	else//右子树不为空
	{
		//右子树的最左节点
		Node* subLeft = _node->_right;
		while (subLeft->_left)
		{
			subLeft = subLeft->_left;
		}
		_node = subLeft;
	}
	return *this;
}
  • 后置++:

后置++和前置++的唯一区别就在于后置++是返回++前的值,这里只需要在前置++的基础上在一开始把当前节点保存起来,直接调用前置++,最后返回保存的那个节点值即可。

//后置++
Self& operator++(int)
{
	Self tmp(*this);
	++(*this);//复用前置++
	return tmp;
}
  • 5、--运算符重载

--运算符又分为前置--和后置--,下面分别讨论:

  • 前置--:

--运算符和++运算符相反,--运算符是找比当前位置次小的节点而这个节点,而这又要优先去左子树里寻找,而左子树又分为如下两种情况:

  • 左子树为空,找祖先里面孩子是父亲右的那个节点
  • 左子树非空,找左子树里最右的节点
//前置--
Self& operator--()
{
	if (_node->_left == nullptr)//左子树为空
	{
		//找孩子是父亲右的那个
		Node* cur = _node;
		Node* parent = cur->_parent;
		while (parent && parent->_left == cur)
		{
			cur = cur->_parent;
			parent = parent->_parent;
		}
		_node = parent;
	}
	else//左子树不为空
	{
		//找左子树的最右节点
		Node* subRight = _node->_left;
		while (subRight->_right)
		{
			subRight = subRight->_right;
		}
		_node = subRight;
	}
	return *this;
}
  • 后置--:

注意后置--返回的是--前的值,所以先定义tmp把*this保存起来,再套用前置--函数进行自减,最后返回tmp。

//后置--
Self& operator--(int)
{
	Self tmp(*this);
	--(*this);
	return tmp;
}

5、红黑树的begin()和end()

迭代器实现后,我们需要在红黑树的实现当中进行迭代器类型的typedef。需要注意的是,为了让外部能够使用typedef后的正向迭代器类型iterator,我们需要在红黑树的public区域进行typedef。

template <class K, class T, class KeyOfT>
class RBTree
{
    //……
public:
	typedef __RBTreeIterator<T, T&, T*> iterator;//普通迭代器
	typedef __RBTreeIterator<T, const T&, const T*> const_iterator;//const迭代器
    //……
}

其实,STL中的红黑树的底层是有一个哨兵位头结点的,如下所示:

STL明确规定,begin()与end()代表的是一段前闭后开的区间,而对红黑树进行中序遍历后,可以得到一个有序的序列,因此:begin()可以放在红黑树中最小节点(即最左侧节点)的位置,end()放在最大节点(最右侧节点)的下一个位置,关键是最大节点的下一个位置在哪块?能否给成nullptr呢?答案是行不通的,因为对end()位置的迭代器进行--操作,必须要能找最后一个元素,此处就不行,因此最好的方式是将end()放在头结点的位置

虽说库里的红黑树实现的是带哨兵位头节点的,但毕竟咱这是模拟(但求大概),综上begin()和end()的指向如下总结:

  • begin():指向红黑树中最小节点(即最左侧节点)的位置
  • end():指向红黑树中最大节点(最右侧节点)的下一个位置,即nullptr
//begin()
iterator Begin()//找最左节点
{
	Node* subLeft = _root;
	while (subLeft && subLeft->_left)
	{
		subLeft = subLeft->_left;
	}
	return iterator(subLeft);//调用迭代器的构造函数
}
//end()
iterator End()//返回最右节点的下一个
{
	return iterator(nullptr);
}

当然这里最好再实现一个const版本的begin()和end(),为的是普通迭代器和const迭代器都能够使用,其实主要还是set的迭代器不能被修改,无论是普通迭代器还是const迭代器,其内部都是用const迭代器封装的,因此必须实现一个const版本的begin()和end()。

//const版本
//begin() 
const_iterator Begin() const//找最左节点
{
	Node* subLeft = _root;
	while (subLeft && subLeft->_left)
	{
		subLeft = subLeft->_left;
	}
	return const_iterator(subLeft);//调用迭代器的构造函数
}
//end()
const_iterator End() const//返回最右节点的下一个
{
	return const_iterator(nullptr);
}

6、红黑树的Find查找函数

查找的规则很简单,只需要遍历节点即可,具体规则如下:

  1. 如果查询的值 > 当前节点值,遍历到右子树查询
  2. 如果查询的值 < 当前节点值,遍历到左子树查询
  3. 如果查询的值 = 当前节点值,返回当前位置的迭代器
  4. 如果循环结束,说明未查询到,返回End()
//Find查找函数
iterator Find(const K& key)
{
	Node* cur = _root;
	KeyOfT kot;
	while (cur)
	{
		if (kot(cur->_data) < key)
		{
			cur = cur->_right;//查询的值 > 节点值,-》右子树
		}
		else if (kot(cur->_data) > key)
		{
			cur = cur->_left;//查询的值 < 节点值,-》左子树
		}
		else
		{
			return iterator(cur);
		}
	}
	return End();
}

7、红黑树源码链接

链接直达:用一颗红黑树封装map和set

实现好了红黑树,接下来就可以套用红黑树继而实现map和set的相关功能函数了,如下。


8、set的代码

#pragma once
#include"RBTree.h"
namespace cpp
{
	template<class K>
	class set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename RBTree<K, K, SetKeyOfT>::const_iterator const_iterator;
		typedef typename RBTree<K, K, SetKeyOfT>::const_iterator iterator;
		//begin()
		iterator begin() const
		{
			return _t.Begin();
		}
		//end()
		iterator end() const
		{
			return _t.End();
		}
		//insert插入
		pair<iterator, bool> insert(const K& key)
		{
			//pair<typename RBTree<K, K, SetKeyOfT>::iterator, bool> ret = _t.Insert(key);
			auto ret = _t.Insert(key);
			return pair<iterator, bool>(iterator(ret.first._node), ret.second);
		}
		//Find查找
		iterator find(const K& key)
		{
			return _t.Find();
		}
	private:
		RBTree<K, K, SetKeyOfT> _t;
	};
}

9、map的代码

#pragma once
#include"RBTree.h"
namespace cpp
{
	template<class K, class V>
	class map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<K, V>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::iterator iterator;
		typedef typename RBTree<K, pair<K, V>, MapKeyOfT>::const_iterator const_iterator;
		//begin()
		iterator begin() 
		{
			return _t.Begin();
		}
		//end()
		iterator end() 
		{
			return _t.End();
		}
		//insert插入
		pair<iterator, bool> insert(const pair<K, V>& kv)
		{
			return _t.Insert(kv);
		}
		//Find查找
		iterator find(const K& key)
		{
			return _t.Find();
		}
		//operator[]
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert(make_pair(key, V()));
			return ret.first->second;
		}
	private:
		RBTree<K, pair<K, V>, MapKeyOfT> _t;
	};
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C++ STL中,红黑树的实现被封装在`std::map`和`std::set`这两个容器类中。这两个容器类都是基于红黑树实现的,它们提供了高效的查找、插入和删除操作,保证了元素的有序性。 STL中的红黑树实现与你提供的C++代码略有不同。STL中的红黑树使用节点颜色(红色或黑色)和节点指针(parent、left、right)来表示树的结构,而你提供的代码使用了模板和节点对象来实现。 在STL中,红黑树的插入和删除操作已经被封装在`std::map`和`std::set`中,使用起来非常简单。你只需要包含相应的头文件`<map>`或`<set>`,并使用`std::map`或`std::set`类来定义变量,就可以直接使用红黑树的功能了。 以下是使用STL中红黑树的简单示例: ```cpp #include <map> int main() { std::map<int, std::string> myMap; // 插入元素 myMap.insert(std::make_pair(1, "one")); myMap = "two"; // 查找元素 auto it = myMap.find(1); if (it != myMap.end()) { std::cout << it->second << std::endl; // 输出 "one" } // 删除元素 myMap.erase(2); return 0; } ``` 在上面的示例中,我们使用`std::map`来创建一个键-值对的红黑树。我们使用`insert`函数插入元素,使用`find`函数查找元素,使用`erase`函数删除元素。 总结一下,C++ STL中的红黑树实现被封装在`std::map`和`std::set`中,使用起来非常方便。你可以直接包含相应的头文件,并使用这些类来实现红黑树的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三分苦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值