简介
关于map和set的介绍和红黑树,之前博客都有介绍。
- map和set:C++ STL -->set和map的使用
- 红黑树:红黑树(RB-Tree)
这篇文章要用一棵红黑树同时封装出set和map,主要利用泛型编程的思想来完成。 之前博客实现的红黑树是K,V模型的,而set是K模型,map是K,V模型。(K模型和KV模型,是二叉搜索树的两个主要应用的两个大模型。关于K和KV模型,在二叉搜索树的最后应用场景有介绍:二叉搜索树(BST))
要用同一棵红黑树来封装map和set,使用模板参数来确定树中存放的是K还是KV模型。 对之前的红黑树进行修改如下:
(删除了验证红黑树相关成员函数,添加了析构和查找函数)
红黑树源码
            
            
              cpp
              
              
            
          
          enum Color
{
	RED,
	BLACK
};
template <class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	T _data;//存储元素
	Color _color; //使用枚举值定义结点的颜色
	RBTreeNode(const T& data)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_data(data)
		,_color(RED)
	{}
};
template <class K, class T>
class RBTree
{
public:
	typedef RBTreeNode<T> Node;
	bool insert(const T& data)
	{
		//空树直接做为根结点
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_color = BLACK;
			return true;
		}
		//1、 确定插入的位置
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			if (data < cur->_data)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (data > cur->_data)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return false;//键值冗余不允许插入
			}
		}
		//2、进行链接
		cur = new Node(data);
		if (data < parent->_data)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;
		//3、若插入结点的父结点是红色的,则需要对红黑树进行调整
		while (parent != nullptr && parent->_color == RED)
		{
			Node* grandfahter = parent->_parent; //parent为红色,grandfahter一定存在
			if (grandfahter->_left == parent) //parent是grandfather左孩子的情况
			{
				Node* uncle = grandfahter->_right;//uncle若存在,一定是其右孩子
				if (uncle != nullptr && uncle->_color == RED)//情况一:u存在且为红
				{
					//颜色调整
					parent->_color = BLACK;
					uncle->_color = BLACK;
					grandfahter->_color = RED;
					//继续向上调整
					cur = grandfahter;
					parent = cur->_parent;
				}
				else //情况2+3(u不存在/u存在且为黑)
				{
					//cur是parent的左
					/*    g
					*   p    u
					* c
					*/
					if (cur == parent->_left)
					{
						//右旋
						RotateR(grandfahter);
						//更新颜色
						parent->_color = BLACK;
						grandfahter->_color = RED;
					}
					else//cur是parent的右
					{
						/*    g
						*   p    u
						*     c
						*/
						//左右双旋(先以p为旋点左旋,在以g为旋点右旋)
						RotateL(parent);
						RotateR(grandfahter);
						// cur变黑,g变红
						cur->_color = BLACK;
						grandfahter->_color = RED;
					}
					break;
				}
			}
			else //parent是grandfather的右孩子
			{
				Node* uncle = grandfahter->_left; //uncle若存在一定是其左孩子
				if (uncle != nullptr && uncle->_color == RED)//u存在且为红
				{
					//颜色调整
					parent->_color = BLACK;
					uncle->_color = BLACK;
					grandfahter->_color = RED;
					//继续向上调整
					cur = grandfahter;
					parent = cur->_parent;
				}
				else//u不存在/u存在为黑
				{
					//cur是parent的右
					/*   g
					*  u   p 
					*		 c
					*/
					if (cur == parent->_right)
					{
						//左旋
						RotateL(grandfahter);
						// p变黑,g变红
						parent->_color = BLACK;
						grandfahter->_color = RED;
					}
					else
					{
						//cur是parent的左
						/*   g
						*  u   p
						*	 c
						*/
						//右左双旋(先以p为轴点右旋,再以g为轴点左旋)
						RotateR(parent);
						RotateL(grandfahter);
						// cur变黑,g变红
						cur->_color = BLACK;
						grandfahter->_color = RED;
					}
					break;
				}
			}
		}
		//根节点一定为黑
		_root->_color = BLACK;
		return true;
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* parent_parent = parent->_parent;
		//让subRL结点作为parent结点的右子树 更新完之后处理subRL_parent;
		parent->_right = subRL;
		if (subRL != nullptr)
		{
			subRL->_parent = parent;
		}
		//让parnet做为subR的左子树 更新完之后处理parent的_parent
		subR->_left = parent;
		parent->_parent = subR;
		//subR做为这颗最小不平衡子树的根节点
		if (parent_parent == nullptr)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (parent_parent->_left == parent)
			{
				parent_parent->_left = subR;
			}
			else
			{
				parent_parent->_right = subR;
			}
			subR->_parent = parent_parent;
		}
	}
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* parent_parent = parent->_parent;
		//让subLR节点做为parent节点的左子树 更新完之后处理subLR的_parent;
		parent->_left = subLR;
		if (subLR != nullptr)
		{
			subLR->_parent = parent;
		}
		//让parent节点做为subL的右子树 更新完之后处理parent的_parent
		subL->_right = parent;
		parent->_parent = subL;
		//让这颗最小不平衡子树的parent节点做为subL的右子树
		if (parent_parent == nullptr)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (parent_parent->_left == parent)
			{
				parent_parent->_left = subL;
			}
			else
			{
				parent_parent->_right = subL;
			}
			subL->_parent = parent_parent;
		}
	}
private:
	Node* _root = nullptr;
};利用模板参数用一棵红黑树同时封装出map和set
将红黑树的第二个模板参数修改为T,通过set和map实例化时确认,如果是set则实例化为Key,如果是map则实例化为pair
- set
            
            
              cpp
              
              
            
          
          namespace ding
{
	template<class K>
	class set
	{
	public:
		//set提供的方法...
	private:
		RBTree<K, K> _tree;实例化为K模型
	};
}- map
            
            
              cpp
              
              
            
          
          namespace ding
{
	template<class K, class V>
	class map
	{
	public:
		//map提供的方法...
	private:
		RBTree<K, pair<const K,V>> _tree;//实例化KV模型(即pair键值对)
	};
}具体实例化如下:  然后红黑树中的节点类,根据模板参数T来确定节点中存放K还是pair
 然后红黑树中的节点类,根据模板参数T来确定节点中存放K还是pair
            
            
              cpp
              
              
            
          
          template <class T>
struct RBTreeNode
{
	RBTreeNode<T>* _left;
	RBTreeNode<T>* _right;
	RBTreeNode<T>* _parent;
	T _data;//存储元素
	Color _color; //使用枚举值定义结点的颜色
	RBTreeNode(const T& data)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_data(data)
		,_color(RED)
	{}
};红黑树中插入元素时,需要通过比较逻辑来确定最终插入位置,而map中的第二个模板参数pair的比较规则不符合map的比较规则
 pair的比较规则单拿小于来说,first或second中有一个小就小。这很明显不符合map的比较规则。
 pair的比较规则单拿小于来说,first或second中有一个小就小。这很明显不符合map的比较规则。
- map的比较规则
 map是用pair中的first进行比较的。所以,在封装时还要考虑比较的规则
利用仿函数进行比较
对于set来说,无所谓,模板参数都是K,直接用来比较即可,但是对于map来说,需要键值对pair中的first来进行比较,而pair的比较规则又不满足比较规则。所以这里利用仿函数和模板参数来解决这一问题
- map
 map仿函数主要返回pair的first用来进行比较
            
            
              cpp
              
              
            
          
          namespace ding
{
	template<class K, class V>
	class map
	{
		//仿函数	
		class MapKeyofT 
		{
		public:
			const K& operator()(const pair<const K, V>& kv )
			{
				return kv.first;
			}
		};
	public:
		//map提供的方法...
	private:
		RBTree<K, pair<const K,V>, MapKeyofT> _tree;
	};
}- set
 set的仿函数可有可无,但是为了和map使用同一棵红黑树,也要提供这个仿函数做为红黑树的第三个模板参数。
            
            
              cpp
              
              
            
          
          namespace ding
{
	template<class K>
	class set
	{
		//仿函数
		class SetKeyofT
		{
		public:
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		//set提供的方法...
	private:
		RBTree<K, K, SetKeyofT> _tree;//实例化为K模型
	};
}红黑树此时就需要第三个模板参数KeyofT,就是为了拿到map中pair的first进行比较。 这里以find为例子(需要比较的地方都需要用仿函数对象来进行比较):
            
            
              cpp
              
              
            
          
          template <class K, class T, class KeyofT>
class RBTree
{
public:
	typedef RBTreeNode<T> Node;
	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur != nullptr)
		{
			if (_kot(cur->_data) > key)
			{
				cur = cur->_left;
			}
			else if (_kot(cur->_data) < key)
			{
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}
		return nullptr;
	}
	Node* _root = nullptr;
	KeyofT _kot;//仿函数对象
};具体实例化过程如下:  以上就是map和set的基本框架了。
 以上就是map和set的基本框架了。
迭代器
map和set的迭代器封装了红黑树的迭代器,而红黑树的迭代器是对红黑树节点指针的封装。
红黑树的迭代器设计和list的迭代器设计基本一样,list的迭代器在这前博客中有详细的介绍:C++ STL -->list模拟实现
- 第一个模板参数T:数据类型(int,char,string等)
- 第二个模板参数Ref:数据类型的引用,即T&
- 第三个模板参数Ptr:数据类型的指针,即T*
            
            
              cpp
              
              
            
          
          template<class T, class Ref,class Ptr>
struct RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef RBTreeIterator< T,  Ref,  Ptr> Self;
	//构造函数
	RBTreeIterator(Node* node);
	Ref operator*();
	Ptr operator->();
	bool operator!=(const Self& s);
	Self operator++();
	Self operator++(int);//后置++重载
	Self operator--();
	Self operator--(int);//后置--重载
		
	Node* _node;
};构造函数
迭代器就是对结点指针进行封装,这里只需要一个结点指针成员变量初始化即可。
            
            
              cpp
              
              
            
          
          RBTreeIterator(Node* node)
    :_node(node)
    {}*运算符重载
解引用操作符,是想拿到地址的内容,直接返回当前结点的数据内容引用即可。 返回值是Ref即T&。
            
            
              cpp
              
              
            
          
          Ref operator*()
{
    return _node->_data;
}->运算符重载
->返回值是指针类型Ptr即T*,这里直接返回对应结点数据的指针即可。
            
            
              cpp
              
              
            
          
          Ptr operator->()
{
    return &(_node->_data);
}前置++运算符重载
红黑树的迭代器++,根据红黑树中序遍历序列找到当前结点的下一个结点。 比如下面红黑树:  如果迭代器it的位置在1处,经过++it后,下一个结点是6,在经过++it后,就是8。 具体的逻辑如下:
 如果迭代器it的位置在1处,经过++it后,下一个结点是6,在经过++it后,就是8。 具体的逻辑如下:
- 
如果当前结点的右子树不为空,++后的结点是右子树最左结点 
- 
如果当前结点的右子树为空,++后的结点是其父节点的父结点,并且孩子节点是父结点的右孩子。比如下面这颗红黑树,迭代器位置在11时,++it后,下一个节点就是13。  
            
            
              cpp
              
              
            
          
          Self operator++()
{
    Node* cur = _node;
    if (cur->_right != nullptr)
    {
            //找最右子树的最左节点
            Node* subRight = cur->_right;
            while (subRight != nullptr && subRight->_left != nullptr)
            {
                    subRight = subRight->_left;
            }
            _node = subRight;
    }
    else
    {
            //如果当前结点的右子树为空,++后的结点是其父节点,并且孩子节点是父结点的左孩子
            //如果 
            Node* parent = cur->_parent;
            while (parent != nullptr && cur == parent->_right )
            {
                    cur = parent;
                    parent = parent->_parent;
            }
            _node = parent;
    }
    return *this;
}前置--运算符重载
一个正向迭代器进行--操作时,应该根据红黑树中序遍历的序列找到当前结点的前一个结点。 比如下面这棵红黑树:  如果迭代器it的位置在27,经过--之后,前一个节点是25,在经过--it之后,前一个结点是22,在经过--it后,前一个结点是17。
 如果迭代器it的位置在27,经过--之后,前一个节点是25,在经过--it之后,前一个结点是22,在经过--it后,前一个结点是17。
- 如果当前结点的左子树不为空,则--操作后应该找到其左子树当中的最右结点。
- 如果当前结点的左子树为空,则--操作后应该在该结点的祖先结点中,找到孩子不在父亲左的祖先。
            
            
              cpp
              
              
            
          
          Self operator--()
{
        Node* cur = _node;
        if (cur->_left != nullptr)
        {
                Node* subLeft = cur->_left;
                while (subLeft != nullptr && subLeft->_right != nullptr)
                {
                        subLeft = subLeft->_right;
                }
                _node = subLeft;
        }
        else
        {
                Node* parent = cur->_parent;
                while (parent != nullptr && cur == parent->_left)
                {
                        cur = parent;
                        parent = parent->_parent;
                }
                _node = parent;
        }
        return *this;
}!=运算符重载
双目运算符,两个迭代器类型对象进行比较。
            
            
              cpp
              
              
            
          
          bool operator!=(const Self& s)
{
    return _node != s._node;
}具体实例化过程:

map的封装
map的[ ]运算符重载
STL源码中,map提供了[ ]运算符,函数原型为:
            
            
              cpp
              
              
            
          
          mapped_type& operator[] (const key_type& k); \]的参数就是一个键值。 \[ \]的返回值是maaped_type的引用。 这里的maaped_type就是上面封装map的第二个模板参数V。  
\[ \]运算符重载主要依靠insert函数。
**operator\[\]的原理是:** 用构造一个键值对,然后调用insert()函数将该键值对插入到map中 如果key已经存在,插入失败,insert函数返回该key所在位置的迭代器 如果key不存在,插入成功,insert函数返回新插入元素所在位置的迭代器 operator\[\]函数最后将insert返回值键值对中的value返回
这里还要修改红黑树insert的返回值如下:
```cpp
pair