Bootstrap

【C++】红黑树详解并封装实现map和set


我们之前已经了解过了AVL树,那么接下来我们将介绍另一种二叉搜索树–红黑树。如果说AVL树是天才发现的,那么红黑树就是天才中的天才创造出来的。为什么这么说呢?接下里就随这篇文章一起来看看吧。

在这里插入图片描述

💡概念及定义

📕红黑树的概念

红黑树,顾名思义就是在每个结点上增加一个存储位表示结点颜色,其是一颗最长路径的长度不超过最短路径的长度的2倍的二叉搜索树,因而红黑树是近似平衡的。在红黑树中,为了便于规范,将空结点(NIL)认为是叶子节点,保证叶子结点一定是黑色的。
在这里插入图片描述

📕红黑树的性质

  • 1.每个结点不是红色就是黑色
  • 2.根节点是黑色的
    1. 如果一个结点是红色的,则它的两个孩子结点一定是黑色的
    1. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  • 5.每个叶子结点都是黑色的(需要注意的是,这里的叶子节点指的是空结点)
    由性质三知道红黑树中不存在连续的两个红色结点,由此推出在一条路径上红色结点的数量一定不会超过黑色结点的数量;而由性质四知道红黑树的每条路径上都有着相同数量的黑色结点。进一步的,我们可以得出结论:红黑树的最短路径为全是黑色结点,最长路径为全是红色结点。如此即可保证红黑树的最长路径不超过最短路径的两倍。
    ⏰【补充】路径的概念:树的路径为根节点到叶子结点的那条路径(但是在红黑树的概念理解中,我们将空结点认为是叶子结点)

📕红黑树的结点定义

与AVL树并无太大的不同,红黑树的结点只是在AVL树的基础上增加了颜色的定义(这里我们还是使用key-value模型的二叉搜索树),其中颜色用枚举表示:

enum Color
{
   
	BLACK,
	RED
};
template <class T>
//template <class K, class V>
struct RBTreeNode
{
   
	typedef RBTreeNode<K, V> Node;

	Node* _left;
	Node* _right;
	Node* _parent;
	//T _data;
	pair<K, V> _kv;
	Color _color;

	//RBTreeNode(const T& data)
	RBTreeNode(const pair<K, V> kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		//,_data(data)
		,_kv(kv)
		,_color(RED)
	{
   }
};

在这里结点颜色默认设置为红色,是为了方便后续插入,在保证红黑树性质的情况下,通过插入红色结点可以更好的保证性质四不被破坏,而如果插入黑色结点,那么保证每条路径的黑色结点数量相等操作将非常繁琐。

📕红黑树的结构

template <class K, class V>

class RBTree
{
   
public:
	//typedef RBTreeNode<T> Node;
	typedef RBTreeNode<K,V> Node;
	RBTree()
		:_root(nullptr)
	{
   }
private:
	Node* _root;
};

其实在STL中,红黑树的实现是还有一个header结点的,其中根节点的_parent指向header,header的_parent也指向根节点;而header的_left指向红黑树中最小的结点(最左下的结点),_right指向红黑树中最大的结点(最右下的结点),但为了简便,我们这里就不实现header结点的相关功能了。
在这里插入图片描述

📕红黑树的应用

红黑树最典型的应用就是实现C++STL中的map和set;其次还有Java库,Linux内核以及其他的一些内核都是用红黑树实现的。

📕红黑树与AVL树的比较

红黑树与AVL树都是平衡的二叉树:

  • AVL树是严格平衡的,而红黑树是近似平衡的
  • AVL树和红黑树的查找时间复杂度都是O(log2N)
  • 由于红黑树旋转次数更少,因此在增删过程中性能较优

💡插入操作

在AVL树的基础下,红黑树的插入操作其实就不难了。

🎄1、寻找要插入的位置

此步我们这钱已经多次介绍介绍,即若key值大于当前结点的key值,则向右寻找;若小于,则向左寻找;若相等,说明数据冗余,返回false。

🎄2.判断是否符合红黑树的规则

由于我们产生新结点时将其颜色设置为红色,而每条路径上黑色结点数量相等这条规则就交给调整时保证。
故判断是否为红黑树就转换为判断是否存在连续红色的结点。

  • 对于新插入的结点,若其父亲颜色为黑色,则满足红黑树的规则,无需调整。
  • 而若其父亲颜色为红色,则规则被破坏,需要调整。

🎄3.对于规则被破坏的情况,进行调整

在规则被破坏的前提下, 总共存在三种情况:1.只需变色;2.单旋+变色;3.双旋+变色。由于旋转操作在AVL树中已经详细介绍,若不清楚则可参考之前AVL树中关于旋转的内容:AVL树的旋转

  • 情况一:若parent,grandparent以及ubcle结点存在,其中p、u为红色,g为黑色,此时只需变色,将parent和uncle变为黑色,grandparent变为红色,由于g所在的树可能为一棵子树,故此时仍需向上调整。
    在这里插入图片描述

  • 情况二:p存在为红色,g存在为黑色,u存在为黑色/u不存在,其中旋转为单旋情况。左边高->右单旋;若右边高->左单旋,旋转完后,p变为黑色,g变为红色
    在这里插入图片描述

  • 情况三:p存在为红色,g存在为黑色,u存在为黑色/u不存在,其中cur为双旋情况。双旋之后,cur为黑色,parent和grandparent为红色。在这里插入图片描述

🎄插入操作代码实现

综上所述,红黑树的代码实现如下:

	bool Insert(const pair<K,V>& kv)
	{
   
		if (_root == nullptr)
		{
   
			//_root = new Node(data);
			_root = new Node(kv);

			_root->_color = BLACK;
			return true;
		}
		Node* cur = _root;
		Node* parent = _root;
		//找到要找到的位置
		while (cur)
		{
   
			//KeyOfT kot;
			//if (kot(cur->_data) < kot(data))
			if(cur->_kv.first < kv.first)
			{
   
				parent = cur;
				cur = cur->_right;
			}
			//else if(kot(cur->_data) > kot(data))
			else if(cur->_kv.first > kv.first)
			{
   
				parent = cur;
				cur = cur->_left;
			}
			else//数据冗余,插入失败
			{
   
				return false;
			}
		}
		//cur为要插入的位置