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 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 insert(const pair& kv) { return _tree.insert(kv); } V& operator[](const K& key) { //1、调用insert函数插入键值对 ret键值对接收insert返回值 pair ret = insert(make_pair(key, V()); //2.拿到插入元素的迭代器 iterator it = ret.first; //3.返回迭代器位置的实值的引用 return it->second; } ``` ## 封装后的map ```cpp namespace ding { template class map { //仿函数 class MapKeyofT { public: const K& operator()(const pair& kv ) { return kv.first; } }; public: typedef typename RBTree, MapKeyofT>::iterator iterator; typedef typename RBTree, MapKeyofT>::const_iterator const_iterator; pair insert(const pair& kv) { return _tree.insert(kv); } V& operator[](const K& key) { //1、调用insert函数插入键值对 ret键值对接收insert返回值 pair 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, MapKeyofT> _tree; }; } ``` # set的封装 ```cpp namespace ding { template class set { //仿函数 class SetKeyofT { public: const K& operator()(const K& key) { return key; } }; public: typedef typename RBTree::const_iterator iterator; typedef typename RBTree::const_iterator const_iterator; //set提供的方法... iterator begin() { return _tree.begin(); } iterator end() { return _tree.end(); } pair insert(const K& key) { return _tree.insert(key); } iterator find(const K& key) { return _tree.Find(key); } private: RBTree _tree;//实例化为K模型 }; } ``` ## set的规则 STL库中不能通过set的迭代器改变set元素的值,因为set的元素值就是其键值。如果修改set元素的值,会破坏set的排序规则。 但是上面封装的set是可以通过迭代器修改set元素的值,STL源码在设计时,将普通迭代器也设计成为了cosnt迭代器。这样就不能通过set的迭代器来改变set的值。 上面set的封装第16行修改为: ```cpp typedef typename RBTree::const_iterator iterator; ``` 这样修改完后,还是会有问题对于下面代码: ```cpp void test_set() { set 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; } ``` 出现了这样的报错信息: ![image.png](https://file.jishuzhan.net/article/1758372507338936322/86491623f2735e697529f84a6019f104.webp) 这里因为set里面的红黑树对象是非const对象,让非const对象去调用红黑树类的成员函数,会调用非const版本的beign,红黑树类非const版本的begin和end函数返回值是iterator,而set中的iterator本质是一个const_iterator。所以会导致无法从非const转换为const的错误。 ![image.png](https://file.jishuzhan.net/article/1758372507338936322/364acfc3ebd1efbce7e5c7eb792a0cbd.webp) 这种情况第一时间想到的是将红黑树类中的非const版本的begin和end删除,只留下const版本的begin和end。 `这种做法针对set是完全可以的,但是map是可以修改键值对中的V的。而set和map共用一棵红黑树,这样做就会导致map无法修改。`只能另寻他路。 STL源码大佬的做法是在迭代器类中提供一个构造函数,如下: ```cpp template struct RBTreeIterator { typedef RBTreeIterator iterator; typedef RBTreeIterator const_iterator; RBTreeIterator(const iterator& it) :_node(it._node) {} //... } ``` * 这个构造函数如果是非cosnt版本的迭代器就是拷贝构造函数 类模板实例化为iterator,他就是一个拷贝构造函数 函数原型如下: ```cpp RBTreeIterator(const RBTreeIterator& it) :_node(it._node) {} ``` 完全符合拷贝构造函数的性质。 * 如果是const版本的迭代器就是一个支持用iterator构造初始化为const_iterator的构造函数。(`这里还涉及到单参数的构造函数支持隐式类型转化,可以将非cosnt的iterator隐式转换为cosnt_iterator`) 函数原型如下: ```cpp RBTreeIterator(const RBTreeIterator& it) :_node(it._node) {} ``` 不符合拷贝构造函数的要求,可以当作是将iterator类型初始化为const_iterator类型的构造函数。 这样就可在红黑树类中的begin或end函数return时调用这个构造函数,将iterator转换为const_iterator类型。从而限制了通过迭代器修改set中的元素。 # 参考源码 完整源码在gitee上。 * gitee [用一颗红黑树封装出map和set](https://link.juejin.cn?target=https%3A%2F%2Fgitee.com%2FFBLbaigou%2Fgitee-study%2Fcommit%2Fafb1e7030f24beed7e152f10103d32e30800042a "https://gitee.com/FBLbaigou/gitee-study/commit/afb1e7030f24beed7e152f10103d32e30800042a") * 菜鸟一枚,写的不好的地方请各位大佬多多包涵,手下留情。

相关推荐
知然7 小时前
鸿蒙 Native API 的封装库 h2lib_arkbinder
c++·arkts·鸿蒙
十五年专注C++开发7 小时前
Qt .pro配置gcc相关命令(三):-W1、-L、-rpath和-rpath-link
linux·运维·c++·qt·cmake·跨平台编译
Cai junhao8 小时前
【Qt】Qt控件
开发语言·c++·笔记·qt
uyeonashi8 小时前
【QT系统相关】QT网络
开发语言·网络·c++·qt
我命由我123459 小时前
嵌入式 STM32 开发问题:烧录 STM32CubeMX 创建的 Keil 程序没有反应
c语言·开发语言·c++·stm32·单片机·嵌入式硬件·嵌入式
筏.k10 小时前
C++: 类 Class 的基础用法
android·java·c++
C++ 老炮儿的技术栈10 小时前
手动实现strcpy
c语言·开发语言·c++·算法·visual studio
一条叫做nemo的鱼10 小时前
从汇编的角度揭开C++ this指针的神秘面纱(下)
java·汇编·c++·函数调用·参数传递
ComputerInBook11 小时前
理解 C++ 的 this 指针
开发语言·c++·指针·this·this指针
MikeWe11 小时前
一文读懂C++移动语义和完美转发
c++