C++红黑树

本期我们来手撕红黑树,相信大家很早就听过红黑树的传说了吧,这里最好有AVL树的基础,我这里把AVL的相关博客贴在这里,没有看过的同学建议先看看

C++AVL树_KLZUQ的博客-CSDN博客

话不多说,我们直接进入正题

目录

红黑树的概念

[​编辑 红黑树的性质](#编辑 红黑树的性质)

代码实现

完整代码


红黑树的概念

红黑树 ,是一种 二叉搜索树 ,但 在每个结点上增加一个存储位表示结点的颜色,可以是 Red Black 。 通过对 任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍 ,因而是 接近平衡 的。

红黑树的性质

  1. 每个结点不是红色就是黑色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个孩子结点必须是黑色的
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  5. 每个叶子结点都是黑色的 ( 此处的叶子结点指的是空结点 )

从上面的性质我们可以分析出,在红黑树中,任意一条路径都没有连续的红节点,我们再看上面的红黑树图片,这棵红黑树有11条路径,路径的尾部是NULL,也就是图里面的NIL节点,从根节点到NIL节点就是一条路径

红黑树要根据这些条件来做到最长路径不超过最短路径的二倍,是怎么做到的呢?

我们把8下面的节点去掉,再把8变为黑色,按照它的条件,大家想一想

最短路径一定是全黑,最长路径就是一黑一红的重复,而且我们可以分析出,每条路径里黑色节点的占比一定是大于等于1/2的

红黑树是比AVL更优的结构,在同样数量级下,红黑树的高度可能是AVL的二倍,但是一亿个数据也就是30和60的区别,对于cpu来说就是0.001秒和0.002秒的差距(我这里随便举的例),但是AVL树要插入这么多节点是需要进行大量旋转的,因为AVL树的左右高度差不能超过1,而红黑树就不用考虑那么多,所以总体而已红黑树是更优的,现实里也是一样,AVL树是几乎不用的,而红黑树缺被沿用了下来,这些大家了解即可

下面我们来进行代码实现

代码实现

cpp 复制代码
enum Colour
{
	RED,
	BLACK
};
template<class K,class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	Colour _col;
    RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		,_col(RED)
	{}
};

template<class K, class V>
struct RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
            _root->_col = BLACK;
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;
		//...

		return true;
	}
private:
	Node* _root = nullptr;
};

我们先写好框架和insert的部分,这里insert和AVL树没什么区别,我就直接复制过来了,和AVL树相比,这里还多了一个颜色,我们使用枚举类型即可

下面我们想一想插入该怎么办?我们插入新的节点,是插入红色节点还是黑色呢?答案是红色,插入黑色节点,所有的路径都会受到影响(红黑树各路径上黑色节点数相同),而插入红色节点,只有当前路径会受到影响,两权相害取其轻

并且左边这种插入是不受影响的,所以我们插入红色节点

如果插入后,父亲节点是黑色,我们就插入结束,如果父亲是红色,我们就需要进行变化

并且,如果父亲节点是红色,那么一定会有爷爷节点,而且是黑色,下面我们就需要分类讨论

如果父亲有兄弟,并且兄弟是红色, 我们要先把父亲和叔叔变黑,爷爷变红

这里父亲一定要变黑,不然是没有办法解决连续的红节点问题的 ,此时在这个局部的子树里,黑色节点数里不变,连续的红节点问题暂时解决

再看下一步,这里分三种情况讨论

1.如果祖父没有父亲,也就是祖父是根节点,我们就把祖父再变黑

2.如果祖父有父亲,父亲是黑色,那么就结束

3.如果有父亲并且父亲是红色,那么需要我们就把祖父当作新增节点继续向上处理

比如此时,我们只需把parent和uncle变黑,再把grandfather变红,最后再把grandfather变黑即可

不过这里是恰好uncle存在且为红,还有别的情况需要讨论

我们先看现在的情况,此时插入完成,相比之前的树,相当于所有路径都多了一个黑色节点,黑色节点多是好的,我们插入时也会方便

下面我们来看这种情况,uncle不存在,我们插入新节点后,最长路径超过了最短路径的二倍,此时就需要进行旋转

这里是一个简单的右单旋,然后需要变色

我们把25变红,22变黑即可,所以uncle不存在的话,我们需要旋转加变色

所以总结一下红黑树的变化

1.叔叔存在且为红,2.叔叔不存在,3.叔叔存在且为黑

什么时候会有第三种情况?

比如我们在下面两个红节点的四个位置随意插入一个新节点 ,就会出现叔叔存在且为黑

此时,我们先把父亲和叔叔变黑,祖父变红

然后grandfather变成cur,其他的依次改变,此时就是叔叔存在且为黑

接着我们进行左旋,然后把parent变黑,grandfather变红即可

我们对比前面的,就可以发现,这里parent都是要变黑的

最后总结:1.红黑树插入的关键是看uncle,uncle存在且为红,则变色+继续向上处理,2.如果uncle不存在或者存在且为黑,则旋转+变色

上面是具体的图,下面我们来看抽象图

约定**:cur****为当前节点,p为父节点,g为祖父节点,u为叔叔节点**
情况一 : cur 为红, p 为红, g 为黑, u 存在且为红
解决方式:将 p,u 改为黑, g 改为红,然后把 g 当成 cur ,继续向上调整。
我们在这里简单分析一下,第一种情况是abcde都是空,cur是新增,就是我们上面讲的
第二种情况是c/d/e是每条路径有一个黑色节点的红黑树,有4种情况

再推测,a和b就是红节点,在a和b的任意孩子位置新增节点,都会导致变色,出现叔叔存在且为红的情况 ,上面一共有4*4*4*3 = 256种情况(c/d/e每个4种,a和b4种),但无论是哪一种,处理方法都是这样的,再想一想,如果c/d/e是每条路径有两个黑色节点的呢?那情况就太恐怖了,和AVL树是一样的,这里也是无穷无尽的,所以我们需要抽象图来帮助我们理解

cpp 复制代码
        while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)//叔叔存在且为红
				{
					//变色
					uncle->_col = BLACK;
					parent->_col = BLACK;
					grandfather->_col = RED;
					//继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
			}
		}
		_root->_col = BLACK;

我们这里写出代码

情况二: cur为红,p为红,g为黑,u不存在/u****存在且为黑

uncle不存在的很简单,只需要旋转即可

再看叔叔存在且为黑,首先这里cur一定不会是新增,否则这棵树就不是红黑树了,因为右边有两个黑色节点了
所以我们可以确定cur应该是黑色的

它的下面是两个红色节点,在它下面任意一个位置插入都会引发问题

有上面的条件,我们可以推出,c是每条路径包含一个黑色节点的红黑树,这里c有四种情况(如上图所示),d和e可能是空,也可能是红节点(2种),这里要是排列组合的话会有4*2*2*4 = 64种情况,但不管是哪一种,这里都是旋转+变色,下面我们来看具体例子

比如我们在最下面插入一个红色节点,我们需要把父亲和叔叔先变黑

接着进行右旋,最后再把p变黑,g变红即可

这里的旋转就是之前的4种,单旋和双旋各两种,所以我们直接把AVL树那块的左旋和右旋代码拷贝过来,再把平衡因子删掉即可

cpp 复制代码
                else//叔叔不存在/存在且为黑
				{
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}

我们上面举例只举了右旋,双旋也是一样的,这里就不再举例

cpp 复制代码
else//parent在grandfather的右边
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)//叔叔存在且为红
				{
					uncle->_col = BLACK;
					parent->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else//叔叔不存在/存在且为黑
				{
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}

接着我们再对称写出另一半代码即可

下面我们来写一个IsBalance,来看看我们的红黑树是否正确

cpp 复制代码
bool IsBalance()
	{
		return IsBalance(_root);
	}
	bool CheckColour(Node* root,int blacknum,int benchmark)//检查节点颜色
	{
		if (root == nullptr)
		{
			if (benchmark != blacknum)//黑色节点数量不对
				return false;
			return true;
		}
		if (root->_col == BLACK)//检测黑色节点数量
		{
			++blacknum;
		}
		if (root->_col == RED && root->_parent && root->_parent->_col == RED)
			cout << root->_kv.first << "连续红节点" << endl;
			return false;
		
		return CheckColour(root->_left,blacknum, benchmark)
			&& CheckColour(root->_right, blacknum, benchmark);

	}
	bool IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;
		if (root->_col != BLACK)
			return false;
		int benchmark = 0;//黑色节点基准值
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col==BLACK)
				++benchmark;
			cur = cur->_left;
		}
		return CheckColour(root,0, benchmark);
	}

这里我们判断是否有连续红节点,每条路径上的黑色节点数量,黑色节点数量我们使用一个基准值来判断,我们先计算这棵树的最左路径有多少黑色节点,把它作为基准值即可,后续如果不一样,那就说明树有问题

我们简单测试一下,没有问题 ,然后我们再测试一下随机数

这里也没有问题

同样的,红黑树这里我们也只实现insert,删除有点复杂了,如果大家想要去看看删除是如何实现的,可以在网上找一找,或者去看看算法导论这本书

红黑树是非常重要的结构,现在很多地方都在使用红黑树,所以要求大家要掌握红黑树

完整代码

cpp 复制代码
enum Colour
{
	RED,
	BLACK
};
template<class K,class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	Colour _col;
	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		,_col(RED)
	{}
};

template<class K, class V>
struct RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				return false;
			}
		}
		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;
		
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)//parent在grandfather的左边
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)//叔叔存在且为红
				{
					//变色
					uncle->_col = BLACK;
					parent->_col = BLACK;
					grandfather->_col = RED;
					//继续向上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else//叔叔不存在/存在且为黑
				{
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else//parent在grandfather的右边
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)//叔叔存在且为红
				{
					uncle->_col = BLACK;
					parent->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				else//叔叔不存在/存在且为黑
				{
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;
	}

	void RotateL(Node* parent)//左单旋
	{
		Node* cur = parent->_right;
		Node* curleft = cur->_left;
		parent->_right = curleft;
		if (curleft)
		{
			curleft->_parent = parent;
		}
		cur->_left = parent;
		Node* ppnode = parent->_parent;
		parent->_parent = cur;
		if (parent == _root)//parent是根节点
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
	}

	void RotateR(Node* parent)//右单旋
	{
		Node* cur = parent->_left;
		Node* curright = cur->_right;
		parent->_left = curright;
		if (curright)
		{
			curright->_parent = parent;
		}
		Node* ppnode = parent->_parent;
		cur->_right = parent;
		parent->_parent = cur;
		if (ppnode == nullptr)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;
			}
			cur->_parent = ppnode;
		}
	}
	bool IsBalance()
	{
		return IsBalance(_root);
	}
	bool CheckColour(Node* root,int blacknum,int benchmark)//检查节点颜色
	{
		if (root == nullptr)
		{
			if (benchmark != blacknum)//黑色节点数量不对
				return false;
			return true;
		}
		if (root->_col == BLACK)//检测黑色节点数量
		{
			++blacknum;
		}
		if (root->_col == RED && root->_parent && root->_parent->_col == RED)
		{
			cout << root->_kv.first << "连续红节点" << endl;
			return false;
		}

		return CheckColour(root->_left,blacknum, benchmark)
			&& CheckColour(root->_right, blacknum, benchmark);

	}
	bool IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;
		if (root->_col != BLACK)
			return false;
		int benchmark = 0;//黑色节点基准值
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col==BLACK)
				++benchmark;
			cur = cur->_left;
		}
		return CheckColour(root,0, benchmark);
	}
private:
	Node* _root = nullptr;
};

以上即为本期全部内容,希望大家有所收获

如有错误,还请指正

相关推荐
若亦_Royi19 分钟前
C++ 的大括号的用法合集
开发语言·c++
Captain823Jack1 小时前
nlp新词发现——浅析 TF·IDF
人工智能·python·深度学习·神经网络·算法·自然语言处理
Captain823Jack1 小时前
w04_nlp大模型训练·中文分词
人工智能·python·深度学习·神经网络·算法·自然语言处理·中文分词
Aileen_0v02 小时前
【AI驱动的数据结构:包装类的艺术与科学】
linux·数据结构·人工智能·笔记·网络协议·tcp/ip·whisper
是小胡嘛2 小时前
数据结构之旅:红黑树如何驱动 Set 和 Map
数据结构·算法
m0_748255022 小时前
前端常用算法集合
前端·算法
呆呆的猫2 小时前
【LeetCode】227、基本计算器 II
算法·leetcode·职场和发展
Tisfy2 小时前
LeetCode 1705.吃苹果的最大数目:贪心(优先队列) - 清晰题解
算法·leetcode·优先队列·贪心·
余额不足121383 小时前
C语言基础十六:枚举、c语言中文件的读写操作
linux·c语言·算法
ragnwang4 小时前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习