C++ STL->用一棵红黑树封装出map和set

简介

关于map和set的介绍和红黑树,之前博客都有介绍。

这篇文章要用一棵红黑树同时封装出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

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的比较规则。

  • 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的迭代器封装了红黑树的迭代器,而红黑树的迭代器是对红黑树节点指针的封装。

红黑树的迭代器设计和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。 具体的逻辑如下:

  • 如果当前结点的右子树不为空,++后的结点是右子树最左结点

  • 如果当前结点的右子树为空,++后的结点是其父节点的父结点,并且孩子节点是父结点的右孩子。比如下面这颗红黑树,迭代器位置在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。

  • 如果当前结点的左子树不为空,则--操作后应该找到其左子树当中的最右结点。
  • 如果当前结点的左子树为空,则--操作后应该在该结点的祖先结点中,找到孩子不在父亲左的祖先。
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<itertaor, bool> insert(const T& data)
	{

		//空树直接做为根结点
		if (_root == nullptr)
		{
			_root = new Node(data);
			_root->_color = BLACK;
			return make_pair(iterator(_root),true);
		}
		//1、 确定插入的位置
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur != nullptr)
		{
			if (_kot(data) < _kot(cur->_data))
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (_kot(data) > _kot(cur->_data))
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return make_pair(iterator(cur), false);;//键值冗余不允许插入
			}
		}
		//2、进行链接
		cur = new Node(data);
		Node* newnode = cur;
		if (_kot(data) < _kot(parent->_data))
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;

		//3、若插入结点的父结点是红色的,则需要对红黑树进行调整
		//....
		return make_pair(iterator(newnode),true);
	}

operator[]的实现如下:

cpp 复制代码
pair<iterator,bool> insert(const pair<K, V>& kv)
{
        return _tree.insert(kv);
}
V& operator[](const K& key)
{
        //1、调用insert函数插入键值对 ret键值对接收insert返回值
        pair<iterator, bool> ret = insert(make_pair(key, V());
        //2.拿到插入元素的迭代器
        iterator it = ret.first;
        //3.返回迭代器位置的实值的引用
        return it->second;
}

封装后的map

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:
		typedef typename RBTree<K, pair<const K, V>, MapKeyofT>::iterator iterator;
		typedef typename RBTree<K, pair<const K, V>, MapKeyofT>::const_iterator const_iterator;

		pair<iterator,bool> insert(const pair<K, V>& kv)
		{
			return _tree.insert(kv);
		}
		V& operator[](const K& key)
		{
			//1、调用insert函数插入键值对 ret键值对接收insert返回值
			pair<iterator, bool> ret = insert(make_pair(key, V()));
			//2.拿到插入元素的迭代器
			iterator it = ret.first;
			//3.返回迭代器位置的实值的引用
			return it->second;
		}
		iterator find(const K& key)
		{
			return _tree.Find(key);
		}
		

		iterator begin()
		{
			return _tree.begin();
		}

		iterator end()
		{
			return _tree.end();
		}

	private:
		RBTree<K, pair<const K, V>, MapKeyofT> _tree;
	};
}

set的封装

cpp 复制代码
namespace ding
{
	template<class K>
	class set
	{
		//仿函数
		class SetKeyofT
		{
		public:
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename RBTree<K, K, SetKeyofT>::const_iterator iterator;
		typedef typename RBTree<K, K, SetKeyofT>::const_iterator const_iterator;
		//set提供的方法...
		iterator begin()
		{
			return _tree.begin();
		}
		iterator end()
		{
			return _tree.end();
		}
		pair<iterator,bool> insert(const K& key)
		{
			return _tree.insert(key);
		}
		iterator find(const K& key)
		{
			return _tree.Find(key);
		}
		
	private:
		RBTree<K, K, SetKeyofT> _tree;//实例化为K模型
	};
}

set的规则

STL库中不能通过set的迭代器改变set元素的值,因为set的元素值就是其键值。如果修改set元素的值,会破坏set的排序规则。

但是上面封装的set是可以通过迭代器修改set元素的值,STL源码在设计时,将普通迭代器也设计成为了cosnt迭代器。这样就不能通过set的迭代器来改变set的值。

上面set的封装第16行修改为:

cpp 复制代码
typedef typename RBTree<K, K, SetKeyofT>::const_iterator iterator;

这样修改完后,还是会有问题对于下面代码:

cpp 复制代码
void test_set()
{
    set<int> s1;
    int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
    for (auto& e : a)
    {
            s1.insert(e);
    }
    auto it = s1.begin();//拿到set第一个元素的迭代器位置
    cout << *it << endl;
}

出现了这样的报错信息:

这里因为set里面的红黑树对象是非const对象,让非const对象去调用红黑树类的成员函数,会调用非const版本的beign,红黑树类非const版本的begin和end函数返回值是iterator,而set中的iterator本质是一个const_iterator。所以会导致无法从非const转换为const的错误。

这种情况第一时间想到的是将红黑树类中的非const版本的begin和end删除,只留下const版本的begin和end。 这种做法针对set是完全可以的,但是map是可以修改键值对中的V的。而set和map共用一棵红黑树,这样做就会导致map无法修改。只能另寻他路。 STL源码大佬的做法是在迭代器类中提供一个构造函数,如下:

cpp 复制代码
template<class T, class Ref,class Ptr>
struct RBTreeIterator
{

	typedef RBTreeIterator<T, T&, T*> iterator;
	typedef RBTreeIterator<T, const T&, const T*> const_iterator;
	RBTreeIterator(const iterator& it)
		:_node(it._node)
	{}
    //...
}
  • 这个构造函数如果是非cosnt版本的迭代器就是拷贝构造函数
    类模板实例化为iterator,他就是一个拷贝构造函数
    函数原型如下:
cpp 复制代码
RBTreeIterator(const RBTreeIterator<T, T&, T*>& it)
		:_node(it._node)
	{}

完全符合拷贝构造函数的性质。

  • 如果是const版本的迭代器就是一个支持用iterator构造初始化为const_iterator的构造函数。(这里还涉及到单参数的构造函数支持隐式类型转化,可以将非cosnt的iterator隐式转换为cosnt_iterator)
    函数原型如下:
cpp 复制代码
RBTreeIterator(const RBTreeIterator<T, const T&, const T*>& it)
		:_node(it._node)
	{}

不符合拷贝构造函数的要求,可以当作是将iterator类型初始化为const_iterator类型的构造函数。

这样就可在红黑树类中的begin或end函数return时调用这个构造函数,将iterator转换为const_iterator类型。从而限制了通过迭代器修改set中的元素。

参考源码

完整源码在gitee上。

相关推荐
湖南罗泽南18 分钟前
Windows C++ TCP/IP 两台电脑上互相传输字符串数据
c++·windows·tcp/ip
可均可可1 小时前
C++之OpenCV入门到提高005:005 图像操作
c++·图像处理·opencv·图像操作
zyx没烦恼1 小时前
【STL】set,multiset,map,multimap的介绍以及使用
开发语言·c++
机器视觉知识推荐、就业指导1 小时前
基于Qt/C++与OpenCV库 实现基于海康相机的图像采集和显示系统(工程源码可联系博主索要)
c++·qt·opencv
myloveasuka2 小时前
类与对象(1)
开发语言·c++
ROC_bird..3 小时前
STL - vector的使用和模拟实现
开发语言·c++
机器视觉知识推荐、就业指导3 小时前
C++中的栈(Stack)和堆(Heap)
c++
Mr_Xuhhh5 小时前
递归搜索与回溯算法
c语言·开发语言·c++·算法·github
无敌岩雀5 小时前
C++设计模式行为模式———命令模式
c++·设计模式·命令模式
爱吃生蚝的于勒7 小时前
C语言内存函数
c语言·开发语言·数据结构·c++·学习·算法