c++:红黑树

一、红黑树的概念

红黑树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表⽰结点的颜⾊,可以是红⾊或者⿊⾊。通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束,红⿊树确保没有⼀条路径会⽐其他路径⻓出2倍,因⽽是接近平衡的。

1.1.与AVL树的区别

AVL树中左右高度差严格控制在1以内,并且如果引入了平衡因子,要严格控制平衡因子的大小,并且AVL树在插入结点时对结点的旋转次数比红黑树的次数多

1.2.红黑树的规则

1 每个结点不是红⾊就是黑色
2. 根结点是黑色的
3. 如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是黑色的,也就是说任意⼀条路径不会有连续的红⾊结点。
4. 对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的⿊⾊结点

由规则4可知,从根到NULL结点的每条路径都有相同数量的⿊⾊结点,所以极端场景下,最短路径就就是全是⿊⾊结点的路径,假设最短路径⻓度为bh(blackheight)。

由规则2和规则3可知,任意⼀条路径不会有连续的红⾊结点,所以极端场景下,最⻓的路径就是⼀⿊⼀红间隔组成,那么最⻓路径的⻓度为2bh。

综合红⿊树的4点规则⽽⾔,理论上的全⿊最短路径和⼀⿊⼀红的最⻓路径并不是在每棵红⿊树都存在的。假设任意⼀条从根到NULL结点路径的⻓度为x,那么bh<=h<=2
bh ,所以,红⿊树确保最⻓路径不超过最短路径的2倍的
说明:《算法导论》等书籍上补充了⼀条每个叶⼦结点(NIL)都是⿊⾊的规则。他这⾥所指的叶⼦结点不是传统的意义上的叶⼦结点,⽽是我们说的空结点,有些书籍上也把NIL叫做外部结点。NIL是为了⽅便准确的标识出所有路径,《算法导论》在后续讲解实现的细节中也忽略了NIL结点,所以我们知道⼀下这个概念即可

路径的意思就是从根结点到空结点成为一条路径,比如上面这颗红黑树就有六条路径

1.3.红黑树的效率

随便给个样例


假设N是红⿊树树中结点数量,h最短路径的⻓度,那么2^h-1<=N<=2 ^2h-1 。由此推出,h ≈logN, 也就是意味着红⿊树增删查改最坏也就是⾛最⻓路径,那么时间复杂度还是O(logN)。
红⿊树的表达相对AVL树要抽象⼀些,AVL树通过⾼度差直观的控制了平衡。红⿊树通过4条规则的颜⾊约束,间接的实现了近似平衡,他们效率都是同⼀档次,但是相对⽽⾔,插⼊相同数量的结点,红⿊树的旋转次数是更少的,因为他对平衡的控制没那么严格

二、红黑树的实现

这里跟AVL树一样我们只做插入和检测是否为红黑树的代码实现,删除的代码实现有兴趣的可以去看《算法导论》或者《STL源码剖析》中讲解

2.1.插入一个值的大概过程

1.因为是搜索二叉树的插入规则,所以我们只需要观察是否违反红黑树的规则即可
2.如果是空树插入,新增结点是黑色的,如果是非空树,那新增结点一定是红色,因为如果为黑色,那规则三就很难维护要所有路径黑色都要变化,所以违反规则四不能有连续的红色结点相对来说容易维护
3.如果插入红色结点,它的父亲结点为黑,那就没有违反任何规则,就插入结束
4.如果插入红色结点,它的父亲为红,那可以确定,它的上一个结点也就是插入结点的祖父结点一定为黑,那唯一变量就是父亲的兄弟叔叔。
说明:下图中假设我们把新增结点标识为c(cur),c的⽗亲标识为p(parent),p的⽗亲标识为g(grandfather),p的兄弟标识为u(uncle)。

如图这样的样例

2.2.情况一: 只需要变色

c为红,p为红,g为⿊,u存在且为红,则将p和u变⿊,g变红。在把g当做新的c,继续往上更新。
分析: 因为c和p是红色要维护没有连续红色结点的规则三,那p就要变黑,变黑后最左边那条路径多了黑色结点,那g就要变为红维护规则四所有路径的黑色结点数量相同,那根结点到u的这条路径就少了一个黑色结点,所以u要变成黑色结点
情况1只变⾊,不旋转。所以⽆论c是p的左还是右,p是g的左还是右,都是上⾯的变⾊处理⽅式。

跟AVL树类似,我们展⽰了⼀种具体情况,但是实际中需要这样处理的有很多种情况。所以我们只需要看抽象图即可
注意:这里的hb表示的是一条路径总的黑色结点数量
后面的情况全部用抽象图来表示

2.2.情况二: 单旋加变色

要单旋加变色一定是做了一次存粹的变色后,然后将g变为c,然后向上找p和g和u,所以此时的c不是新增结点,
处理情况一:然后再判断p,如果为黑,那就结束,
情况二:如果为红,那就和变色一样看u,u为红继续变色处理,然后向上走
情况三:如果为红,那就和变色一样看u,u为黑,单纯变色会使u的那条路径少一个黑色结点,所以需要旋转加变色

如图的这种情况,p在g的左边,且c在p的左边,可以类比成存粹的左边高,进行右单旋,然后改变p为黑和g为红

或者u结点为空也可以归为这一类

左单旋+变色是同样的思路,如果不懂怎么旋转,大佬们可以看我以前写的AVL树的实现。这里不做解释了大佬可以自行尝试

代码如下:

cpp 复制代码
while (parent && parent->_Col == RED)
{
	node* grandfather = parent->_parent;
	if (parent == grandfather->_left)
	{
		node* uncle = grandfather->_right;
		//cur是新增结点
		//叔叔存在且为红色
		if (uncle && uncle->_Col == RED)
		{
			parent->_Col = BLACK;
			uncle->_Col = BLACK;
			grandfather->_Col = RED;
			cur = grandfather;
			parent = cur->_parent;
		}
		//cur不是新增结点
		//叔叔为空或为黑色
		else {
			//  g
			// p
			//c  u
			// 右旋加变色
			if (cur == parent->_left)
			{
				RotateR(grandfather);
				parent->_Col = BLACK;
				grandfather->_Col = RED;
			}
			else{
			//左右双旋...
			}
			}

2.2.情况三: 双旋加变色

c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。
分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解决问题,需要旋转+变⾊。

1.这里c在p的右边,不是存粹的左边高,就要先进行左旋转将树弄成存粹的左边高,就是先左旋转再右旋转在更新c变为黑色,g变为红色
2.然后p在g的右边且c在p的左边,就是先右旋转再左旋转,最后更新c变为黑色,g变为红色

cpp 复制代码
	while (parent && parent->_Col == RED)
	{
		node* grandfather = parent->_parent;
		if (parent == grandfather->_left)
		{
			node* uncle = grandfather->_right;
			//cur是新增结点
			//叔叔存在且为红色
			if (uncle && uncle->_Col == RED)
			{
				parent->_Col = BLACK;
				uncle->_Col = BLACK;
				grandfather->_Col = RED;
				cur = grandfather;
				parent = cur->_parent;
			}
			//cur不是新增结点
			//叔叔为空或为黑色
			else {
				//  g
				// p
				//c  u
				// 右旋加变色
				if (cur == parent->_left)
				{
					RotateR(grandfather);
					parent->_Col = BLACK;
					grandfather->_Col = RED;
				}
				else {
				//  g
				// p
				//u  c
				// 左右双旋加变色
					RotateL(parent);
					RotateR(grandfather);
					cur->_Col = BLACK;
					grandfather->_Col = RED;
				}
				break;
			}
		}

2.3.总结情况

只要看上面会了单纯变色和右旋转加变色和左右旋转加变色,那左旋转加变色和右左旋转加变色都是类似的思路,这里注意一下,颜色是用枚举结构体,这样颜色不是黑就是红,完整代码如下

cpp 复制代码
enum Color
{
	RED,
	BLACK
};

template <class K, class V>
struct RBNode
{
	pair<K, V> _kv;
	RBNode<K, V>* _left;
	RBNode<K, V>* _right;
	RBNode<K, V>* _parent;
	Color _Col;
	RBNode(const pair<K, V>& key_value)
		:_kv(key_value)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_Col(RED)
	{}

};

//红黑树的实现
template <class K, class V>
class RBTree
{
	typedef RBNode<K, V> node;
public:
	bool insert(const pair<K, V>& key_value)
	{
		if (!_root)
		{
			_root = new node(key_value);
			_root->_Col = BLACK;
		}
		node* cur = _root;
		node* parent = nullptr;
		while (cur)
		{
			if (key_value.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (key_value.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else {
				return false;
			}
		}
		cur = new node(key_value);
		if (key_value.first < parent->_kv.first)
			parent->_left = cur;
		else 
			parent->_right = cur;
		cur->_parent = parent;
		cur->_Col = RED;
		//如果parent的颜色为红色就要变色
		while (parent && parent->_Col == RED)
		{
			node* grandfather = parent->_parent;
			if (parent == grandfather->_left)
			{
				node* uncle = grandfather->_right;
				//cur是新增结点
				//叔叔存在且为红色
				if (uncle && uncle->_Col == RED)
				{
					parent->_Col = BLACK;
					uncle->_Col = BLACK;
					grandfather->_Col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				//cur不是新增结点
				//叔叔为空或为黑色
				else {
					//  g
					// p
					//c  u
					// 右旋加变色
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						parent->_Col = BLACK;
						grandfather->_Col = RED;
					}
					else {
					//  g
					// p
					//u  c
					// 左右双旋加变色
						RotateL(parent);
						RotateR(grandfather);
						cur->_Col = BLACK;
						grandfather->_Col = RED;
					}
					break;
				}
			}
			else {//parent== grandfather-> _right;
				node* uncle = grandfather->_left;
				//cur是新增结点
				//叔叔存在且为红色
				if (uncle && uncle->_Col == RED)
				{
					parent->_Col = BLACK;
					uncle->_Col = BLACK;
					grandfather->_Col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				//cur不是新增结点
				//叔叔为空或为黑色
				else {
					//  g
				    // u  p
					//     c
					// 左单旋+变色
					if (cur == parent->_right)
					{
						RotateL(grandfather);
						parent->_Col = BLACK;
						grandfather->_Col = RED;
					}
					else {
					//  g
					// u  p
					//   c
					//右左双旋+变色
						RotateR(parent);
						RotateL(grandfather);
						cur->_Col = BLACK;
						grandfather->_Col = RED;
					}
					break;
				}
			}
		}
		_root->_Col= BLACK;
		return true;
	}
	void RotateR(node* parent)
{
	node* subL = parent->_left;
	node* subLR = subL->_right;
	node* parent_parent = parent->_parent;
		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		subL->_right = parent;
		parent->_parent = subL;
		if (parent_parent)
		{
			if (parent_parent->_left == parent)
			{
				parent_parent->_left = subL;
			}
			else {
				parent_parent->_right = subL;
			}
			subL->_parent = parent_parent;
		}
		else {
			_root = subL;
			_root->_parent = nullptr;
		}

}
void RotateL(node* parent)
{
	node* subR = parent->_right;
	node* subRL = subR->_left;
	node* parent_parent = parent->_parent;
	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;
	subR->_left = parent;
	parent->_parent = subR;
	if (parent_parent)
	{
		if (parent_parent->_left == parent)
		{
			parent_parent->_left = subR;
		}
		else {
			parent_parent->_right = subR;
		}
		subR->_parent = parent_parent;
	}
	else {
		_root = subR;
		_root->_parent = nullptr;
	}

}

红黑树的检测

我们的检测思路就是要遵守红黑树的四条规则
1、 规则一是否为搜素二叉树的检查,一个中序遍历就能判断
2、规则二也很简单,判断根结点是否为黑就行
3.规则3前序遍历检查,遇到红⾊结点查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲的颜⾊就⽅便多了。
4.规则4前序遍历,遍历过程中⽤形参记录跟到当前结点的blackNum(⿊⾊结点数量),前序遍历遇到⿊⾊结点就++blackNum,⾛到空就计算出了⼀条路径的⿊⾊结点数量。再任意⼀条路径⿊⾊结点数量作为参考值,依次⽐较即可。

cpp 复制代码
void Inorder()
{
	_Inorder(_root);
}
void _Inorder(node* root)
{
	if (!root)
		return;
	_Inorder(root->_left);
	cout << root->_kv.first << ' ';
	_Inorder(root->_right);
}
	bool RBblance()
	{
		if (!_root)
			return true;
		if (_root && _root->_Col == RED)
			return false;
		 int mostleftB = 0;
		node* cur = _root;
		while (cur)
		{
			if (cur->_Col == BLACK) {
				++mostleftB;
			}
			cur = cur->_left;
		}
		return check(_root,0,mostleftB);
	}
private:
	bool check(node* root, int blacknum, const int mostleftB)
	{
		if (!root)
		{
			if (mostleftB != blacknum)
			{
				cout << "存在黑色结点的数量不相等的路径" << endl;
			}
			return true;
		}
		if (root->_Col==RED && root->_parent->_Col == RED)
		{
			cout << root->_kv.first << "->" << "存在连续的红色结点" << endl;
			return false;
		}
		if (root->_Col == BLACK)
		{
			++blacknum;
		}
		return check(root->_left, blacknum, mostleftB)
			&& check(root->_right, blacknum, mostleftB);
	}

递归的思路就是形参传值传参,每次递归形参的改变不会影响实参,也就是说每遍历一个结点就能知道它当前有几个黑色结点,到空结点之后就能知道这条路径有多少个黑色结点,然后先存下某一条路径的黑色结点,然后走到空结点后让每条路径的黑色结点数量与它比较,最后如果左子树和右子树都满足红黑树规则就证明是一颗红黑树