set与map的封装

本文详细介绍了如何使用红黑树实现C++中的set和map,包括它们的底层结构、如何通过模板参数区分K模型和K-V模型、插入操作中的键值比较以及迭代器的实现。特别地,文章强调了在插入时使用仿函数解决比较问题,以及在迭代器中如何处理++和--操作,确保对红黑树的正确遍历。此外,还讨论了map中operator[]的功能及其实现。
摘要由CSDN通过智能技术生成

一:set与map的底层结构

二:用红黑树封装set,map

            2.1:如何区分传过来的数据类型

            2.2:迭代器的实现

            2.3:封装set,map

            2.4:  map中的operator[]

三:总结 

///

///

一:set与map的底层结构:

set与map是关联容器,其底层都是通过红黑树封装,至于set,map的使用,可以通过下面的文档学习,在此不做过多的赘述;

set - C++ Reference                                       map - C++ 参考资料 (cplusplus.com)

在之前红黑树博客中,我们实现一颗K-V模型的红黑树,节点里面存储的应该是键值对pair,

但是set是K模型,map是K-V模型,怎么一棵树可以实现两种不同的模型?不妨去STL库中学习源码,下面给出set,与map的大致源码结构:

 通过对源码的学习,我们发现,set与map确实用了同一颗红黑树,但是传给红黑树的value值却不同,

set是<K,K>,    而map却是<K,  pair<const K,V>>;

如何理解这两个不同的模版参数呢?

1:无论是set,还是map,第一个模版参数K,是拿到一个单独的K类型,这样是方便像Find,Erase等这样的接口,这些接口都是用K来进行搜索或者删除的操作;

2:而第二个模版参数则是决定了存储在红黑树中的数据类型,是K还是K-V类型;

所以,set与map的底层虽然都是红黑树,但是区别就在于给红黑树实例化的是什么类型的模版参数

 map给红黑树传的模板参数是键值对pair<const Key, T>。而 set给红黑树传的模板参数是键值Key。

而对于红黑树而言,此时它就不能直接写死是K模型或者是K-V模型的树,所以只能通过模版解决,通过对第二个模版参数的解读,才能明白到底是set,还是map,同样,我们还是学习一下红黑树的源码,看看它是如何解决这一问题的;

 通过对红黑树源码的观察,我们发现,它通过Value模版参数进行初始化,至于这个Value模版是什么,则需要看传过来的是什么,传过来的是K,那么Value就是K,如果传过来的是键值对K-V,那么Value就是键值对;根据这些,我们来改一改自己的红黑树节点构造,具体如下所示:

	enum Colour
	{
		RED,
		BLACK
	};

	template<class T>//将之前的 键值对 变成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)
		{

		}
	};

///

///

  二:用红黑树封装set,map

2.1:如何区分传过来的数据类型:
如上文所说,红黑树是通过使用模板,通过泛型编程,解决K模型K-V模型的区别,但是在插入时,我们需要比较节点中数据的大小,决定其插入在左树还是右树,

set插入时,通过节点root->K就可以比较大小;

map插入时,则通过节点root->kv.first来进行比较大小(second不参与比较);

那么在比较处到底该写成map和set中的哪种比较方式呢?

要是按map比,不行,要知道set中的data不是键值对,是没有first的,无法比较,

要是按照set直接比,也不行,因为map中的data直接比较又不符合我们的要求。因为map中的data就是键值对pair,pair的比较方法不能满足我们的需要,

 pair的比较中,second将会参与比较,我们只期望通过K(first)进行比较,所以,此时我们就不能很单纯的就觉得直接比较大小,我们需要明白搞清楚传过来的是什么,才好进行比较!

那么此时我们也不能自己重新定义键值对的比较方式,因为人家库中已经有了,我们无法再重载一个函数名,返回值,参数都相同的比较方式。

为了能够在红黑树中使用统一的比较方式,我们可以通过仿函数来解决:

在自己封装的set和map中,都加上一个仿函数setkeyOFT(mapkeyOPT),通过这个仿函数获取键值,set就获取K,map就获取KV.first;然后将这个仿函数当作模版参数传过红黑树,这样的话,当set实例化调用时,传的是set的K,map实例化调用时,传的就是map的KV.first,通过仿函数告知红黑树,那么红黑树就可以用统一的方式比较大小了,具体如下所示:

在插入函数inset中,创建仿函数对象koft。然后在需要进行键值key比较的位置,使用仿函数koft获取键值进行比较,然后决定插入左边还是右边。

下面给出插入的代码:

		bool Insert(const T& data)
		{
			if (_root == nullptr)
			{
				_root = new Node(data);
				_root->_col = BLACK;
				return true;
			}
			Node* parent1 = nullptr;
			Node* cur = _root;

			KeyOFT kot;//定义仿函数,取出_data中要比较的数据,是K或者KV.first;

			while (cur != nullptr)
			{
				if (kot(cur->_data) < kot(data))
				{
					parent1 = cur;
					cur = cur->_right;
				}
				else if (kot(cur->_data) > kot(data))
				{
					parent1 = cur;
					cur = cur->_left;
				}
				else
				{
					//cout << "要插入的值已经存在" << endl;
					return false;
				}
			}
			cur = new Node(data);
			cur->_col = RED;
			if (kot(parent1->_data) < kot(data))
			{
				parent1->_right = cur;
			}
			else //if (kot(parent1->_data) > kot(data))
			{
				parent1->_left = cur;
			}
			cur->_parent = parent1;
			while (parent1 != nullptr && parent1->_col == RED)//插入新节点后开始调整
			{
				Node* grandfather = parent1->_parent;
				if (grandfather->_left == parent1)
				{
					Node* uncle = grandfather->_right;
					if (uncle != nullptr && uncle->_col == RED)
					{
						parent1->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//接着向上调整
						cur = grandfather;
						parent1 = cur->_parent;
					}
					else
					{
						if (parent1->_left == cur)
						{
							//       g
							//    p      u
							// c
							RotateR(grandfather);
							parent1->_col = BLACK;
							grandfather->_col = RED;
						}
						else
						{
							//      g
							//  p       u
							//      c
							RotateL(parent1);
							RotateR(grandfather);
							cur->_col = BLACK;
							parent1->_col = RED;
							grandfather->_col = RED;
						}
						break;
					}
				}
				else if (grandfather->_right == parent1)
				{
					Node* uncle = grandfather->_left;
					if (uncle != nullptr && uncle->_col == RED)
					{
						parent1->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//向上调整
						cur = grandfather;
						parent1 = cur->_parent;
					}
					else
					{
						if (parent1->_right == cur)
						{
							//     g
							// u      p
							//           c
							RotateL(grandfather);
							parent1->_col = BLACK;
							grandfather->_col = RED;
						}
						else
						{
							//      g
							//  u       p
							//      c
							RotateR(parent1);
							RotateL(grandfather);
							cur->_col = BLACK;
							parent1->_col = RED;
							grandfather->_col = RED;
						}
					}
				}
			}
			_root->_col = BLACK;
			return true;
		}

像这样使用仿函数的方法,就不用关心比较的是键值还是键值对,因为set和map都会给红黑树传它自己获取键值的仿函数,然后插入时取出要比较的键值,这样最终比较的就都是键值K。所以仿函数在此是解决这类问题的重要手段;

///

2.2:迭代器的实现:

set和map的迭代器实现和List差不多,可以参考,红黑树的一个一个节点也都是通过指针进行链接的;我们重点只实现主要的,例如“*,->,!=,++,--”这些运算;

 迭代器的具体代码如上所示,看起来其实和List差不多,有三个模版参数,同时,迭代器中有一个节点,通过它来控制迭代器的各种操作;

下面我们开始完善一些常见的运算:

1:“*”和“->”:

//三个模版参数,解决普通迭代器和const迭代器,
	template<class T,class Ref,class Ptr>
	struct RBTree_iterator_Node
	{
		typedef RBTreeNode<T> Node;
		typedef RBTree_iterator_Node<T, Ref, Ptr> self;
		Node* _node;

		//构造函数;
		RBTree_iterator_Node(Node* node)
			:_node(node)
		{

		}

		//解引用operator*重载
		Ref operator*()
		{
			return _node->_data;
		}

		//箭头operator->重载
		Ptr operator->()
		{
			return &_node->_data;
		}

这里的opator*返回的是迭代器中的值,operator->返回的是数据的地址,这里和List是一模一样的,如果不是很明白的话,可以后头复习一下List;

2:“!=” 和“==”

			bool operator!=(const self& t)
		{
			return _node != t._node;
		}

		bool operator==(const self& t)
		{
			return _node == t._node;
		}

 迭代器!=和==比较的指向的节点是否相同,而不是节点里面的值!

3:迭代器的++:

我们先想清楚,迭代器的使用是方便我们对容器进行遍历,那么红黑树也是二叉搜索树,其遍历是按照中序遍历整颗树,所以在我们迭代器的实现中,也一个遵循“左子树,根,右子树”这个基本原则,

 

 同时,我们知道迭代器的开始一定是指向整颗树的最左节点,以上述的树为例,那么begin()一定是从1节点开始的,所以迭代器一开始应该在1节点这里,那么如何向后遍历呢?这里就有两种情况:

1:1节点肯定是最左节点,如果有右节点,那么则需要向右子树遍历,寻找右子树的最左节点;

2:   1节点肯定是最左节点,如果没有右节点,那么则直接去向父节点遍历

根据这两种不同的情况,我们需要不同的分析,

当右子树存在时:

 当右子树存在时,迭代器it访问完1节点之后会跳到右子树的最左节点去,具体代码如下所示:

        self& operator++()
		{
			if (_node->_right != nullptr)//右不为空,
			{
				Node* subleft = _node->_right;
                //找右子树的最左节点,
				while (subleft != nullptr && subleft->_left!= nullptr)
				{
					subleft = subleft->_left;
				}
                //最后更改迭代器位置
				_node = subleft;
			}
			else//右为空
            {
                //。。。。。
            }
        }

当右为空时:

 当右子树不存在时,1节点访问完将跳到其父节点8节点进行访问,当8节点访问完时,将跳到其右子树的最左节点进行访问,但是11节点没有左树,那么此时迭代器改访问哪一个节点呢?

11节点没有左右子树,那么当访问完11节点时,我们该怎样去访问父节点吗?再回头访问父节点8节点吗/

答案是不能的,因为11的父节点8节点我们已经访问过了,通过对图的观察,我们看到,当一个节点存在左子树时,那么它将不能先访问,只能将左树访问完才可能访问根和右子树,

那么对于8节点来说,它的左子树和根已经访问完了,此时迭代器也在它右树的最后一个节点上,所以,以8节点为根节点的整颗子树也就即将访问完了,而对于8节点为根节点的子树来说,它只是13节点的左树而已,当一个节点的左树全部访问完时,下一个就一个访问节点本身了;

所以,此时迭代器下一个位置就在迭代器本身父节点的父节点!

同时,当迭代器走到整颗树的最右节点时,下一步该怎么办呢?

 当it走到了整颗树的最右节点,那么则说明it所在节点的右树的父节点已经访问过了,即25节点已经访问过了,同样,25节点也是作为右树的节点,当25节点整颗树访问结束时,也表明着其所在右树的父节点17节点已经访问过了,那么依次向上推,那么我们可以得到,此时整颗树的节点都已经被访问完了,当根节点被访问完时,我们将要访问它的父节点,而根节点13节点的父节点指向nullptr,那么所以,当迭代器指向nullptr时,表明整颗树已经迭代访问完了!具体代码如下所示:

        self& operator++()
		{
			if (_node->_right != nullptr)//右不为空,
			{
				Node* subleft = _node->_right;
                //找右子树的最左节点,
				while (subleft != nullptr && subleft->_left!= nullptr)
				{
					subleft = subleft->_left;
				}
                //最后更改迭代器位置
				_node = subleft;
			}
			else//右为空
			{
				Node* cur = _node;//记录一下当前节点
				Node* parent = cur->_parent;//记录一下当前节点的父节点
				while (parent != nullptr && parent->_right == cur)//当父节点存在且此时当前节点是父节点的右树,则表明以父节点为头节点的整颗子树访问完成,则更新节点的位置;
				{
					cur = parent;
					parent = cur->_parent;
				}
				_node = parent;//最后让迭代器的位置调到父节点上去
			}
			return *this;
		}

既然有了前置++,那么顺手实现一下后置++:

self& operator++(int)
		{
			self temp = self(_node);
			if (_node->_right != nullptr)
			{
				Node* subleft = _node->_right;
				while (subleft != nullptr && subleft->_left != nullptr)
				{
					subleft = subleft->_left;
				}
				_node = subleft;
			}
			else
			{
				Node* cur = _node;
				Node* parent = cur->_parent;
				while (parent != nullptr && parent->_right == cur)
				{
					cur = parent;
					parent = cur->_parent;
				}
				_node = parent;
			}
			return temp;
		}
	};

前置++与后置++代码上说简直是一模一样,只不过是一个返回++之前的值,应该返回++之后的值;

2.4:迭代器的--:

迭代器的--是与迭代器的++是正好完全相反的,遵循“右子树,根,左子树”的规律即可,其--的原理和++一样;分为了两中情况,

1:当左子树存在,迭代器需要向去左树遍历,找到左子树的最右节点;

2:当左子树不存在,迭代器需要向父节点遍历

所以根据这两种不同的情况,我们需要不同的分析,

当左树不为空:

 当左子树存在时,迭代器--就是将迭代器指向其左子树的最右节点,具体代码如下所示:

        self operator--()
		{
			//左子树存在
			if (_node->_left)
			{
				//寻找左子树最右边节点
				Node* subRight = _node->_left;
				while (subRight->_right)
				{
					subRight = subRight->_right;
				}
				_node = subRight;//it指向左子树最右节点
			}
			else//左子树不存在
            {
                //。。。。。。
            }
         }

 当左树为空时:

 这里的逻辑思想和前置++的思想差不多,就是不断向上遍历找父节点,判断父节点有没有左子树,如果有,按照有左子树的方式处理,如果没有,则继续向上寻找父节点;

同时当迭代器走到左树最小节点1节点时,--it会指向整棵树的根节点,13节点的父节点,也就是将会指向空节点,具体代码如下所示:

self operator--()
		{
			//左子树存在
			if (_node->_left)
			{
				//寻找左子树最右边节点
				Node* subRight = _node->_left;
				while (subRight->_right)
				{
					subRight = subRight->_right;
				}
				_node = subRight;//it指向左子树最右节点
			}
			//左子树不存在
			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 ret = Self(_node);
			//左子树存在
			if (_node->_left)
			{
				//寻找左子树最右边节点
				Node* subRight = _node->_left;
				while (subRight->_right)
				{
					subRight = subRight->_right;
				}
				_node = subRight;//it指向左子树最右节点
			}
			//左子树不存在
			else
			{
				Node* cur = _node;
				Node* parent = cur->_parent;
				while (parent && cur == parent->_left)
				{
					cur = cur->_parent;
					parent = parent->_parent;
				}
				_node = parent;
			}
			return ret;
		}

同时,当迭代器的各种基本运算符搞定后,我们开始实现在红黑树内部的begin(),end(),等;具体代码如下:

        iterator begin()//迭代器的开头一定是整颗树的最左节点,因为红黑树遍历时遵循中序遍历
		{
			Node* cur = _root;
			while (cur != nullptr && cur->_left != nullptr)
			{
				cur = cur->_left;
			}
			return iterator(cur);
		}

		iterator end()  
		{
			return iterator(nullptr);
		}

        //const迭代器
		const_iterator begin() const
		{
			Node* cur = _root;
			while (cur != nullptr && cur->_left != nullptr)
			{
				cur = cur->_left;
			}
			return const_iterator(cur);
		}

		const_iterator end() const
		{
			return const_iterator(nullptr);
		}

///

2.3:封装set,map:

因为我们已经解决了在红黑树中区分K模型KV模型的问题,已经大致解决了迭代器的问题,那么我们先试着将set,map通过红黑树封装起来:

 set和map一样,但是通过调用红黑树的接口来实现其各种功能,但是这里的迭代器却有所不同,为什么呢?

1: set中节点中存放的是key值,所以必然不能被修改,所以它的iterator和const_iterator的底层都是红黑树的迭代器的const迭代器,保证set中的数据不能被修改。
 2: map中节点存放的是键值对,键值对中的key(也就是frist)值不可以被修改,但是另一个Value(也就是second)可以被修改。键值对的first的类型是const K类型,已经保证了key值不会被修改。所以map提供了普通和const两种迭代器,

同时,在set和map调用迭代器的时候需要注意加上typename;

如果不加typename编译器分不清它到底是类型还是静态变量;加上typename就是告诉编译器它是一个类型,

 ​​​​​​​

 当类模板实例化之后,取这个类型,就不用再加typename去表面自己是一个类型了

到此,我们就可以正常的进行插入和迭代器打印了,下面给出个示例:

 

///

 2.4:  map中的operator[]:

map中的operator[]功能十分强大,可以完成修改,插入,查找多项功能,接下来我们实现一下:

		V& operator[](const K& key)//只通过K去检索
		{
            //返回的是一个键值对,iterator表示返回的迭代器的位置
            //插入成功返回true,插入失败false;
			pair<iterator, bool> ret = _m.Insert(make_pair(key, V()));//V是默认构造;
			return ret.first->second;
		}

接下来给出一个示例验证一下:

 

 我们发现,运行报错,因为operator[]返回的是pair<iterator,bool> ,而我们底层自己实现的红黑树返回的是bool值,所以我们需要将底层的插入改一改;

 

 底层的插入改动了,那么封装的set,map的插入也需要跟着改动:

 当全部改好后,我们在运行:

 set的插入报错,这是为什么呢?

因为在上文中,我们提过set中节点里面存的数据是K,K是不允许修改的;所以普通迭代器和const迭代器封装的都是红黑树的const迭代器;

set不像map,map节点里面存的数据是pair<const K,V>,KV.first是不能修改的,但是我们在给模版参数时,就已经给K加上了const,同时,KV.second是支持修改的,所以map的普通迭代器是普通迭代器,const迭代器就还是const迭代器;

而set插入时,调用的是红黑树的插入节点,红黑树的插入接口中,返回值是普通迭代器,而set插入接口的返回类型是const迭代器,所以,才会有上面的报错,无法从普通的迭代器转换为const迭代器!

那么如何解决这个问题呢?我们学习一下红黑树迭代器的源码:

 通过这个用普通迭代器构造const迭代器的构造函数,就可以解决set的返回值和返回值类型不同的问题,当set返回一个普通迭代器,返回类型为const迭代器时,单参数的构造函数支持隐式类型的转换,将普通迭代器转换为const迭代器,完美的解决了这一问题!所以我们将底层红黑树的迭代器完善一下:

 

那么我们再运行一下示例:

 此时,我们就可以正常的使用map中的operator[]了;

三:总结 

///

///

至此,我们大概完成了,set,map的插入与迭代器的实现,我们的目的不是创建一个更好的轮子,将其全部完善,把默认成员函数,查找,删除等都补齐写上,没有必要,我们主要是学会如何通过红黑树去构建的思想,比如插入比较时的仿函数,迭代器中的用普通迭代器构造const迭代器的思想,是值得我们借鉴学习的;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值