平衡二叉树之红黑树

红黑树产生的历史背景

前面我们介绍了二叉搜索树,因为二叉搜索树在极端情况下会退化成一个链表,导致搜索的优势荡然无存。因此人们创造出了AVL树来解决这个问题。 但是由于AVL树的规则太严格,也就意味着每次插入节点对AVL树维护的成本会比较高。所以人们需要一棵相对平衡的二叉树但是维护的成本相比AVL会更小。于是,计算机的先驱们就提出了红黑树,红黑树不仅有着接近AVL的查找性能,并且它的维护成本相较于AVL会小很多。 接下来,我们就来走进红黑树。

红黑树的五个性质的解读

首先,如果一棵二叉搜索树有如下5个性质,那么它就是一棵红黑树:

1.每个节点不是红色就是黑色
2.根节点的颜色是黑色(空节点也可以认为是黑色节点)
3.没有连续的红色节点
4.每条路径上的黑色节点的数量相同
5.最长的路径不超过最短路径长的2倍

接下来,我们来看一看这五条规则是怎么保证红黑树的平衡。
首先规则1没什么好说的,然后就是规则3和规则4 保证了规则5情况,因为在规则3规则4的限制下

最短的是全黑色节点的路径,而最长的路径就是一红一黑交替的路径。设纯黑色的路径有x个黑色节点,那么最长的路径就是2x个节点,刚好就是性质5。

因此,红黑树也可保证相对的平衡。在查找的效率上也可以接近O(logn)。下面我们就来模拟实现红黑树。

红黑树的插入算法

首先我们先来定义红黑树的节点结构:

//红黑树的节点结构
/*
*模拟实现红黑树
* 1.每个节点不是黑色就是红色
* 2.根节点是黑色
* 3.没有连续的红节点
* 4.每条路径有相同数量的黑节点
* 5.最长路径不超过最短路径的2倍
*/
//使用枚举类型,红色和黑色
enum Colour
{
	RED,
	BLACK,
};
template<typename K,typename V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	Colour _col;
	//expilict限制隐式类型转换,可加可不加
	explicit RBTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED)
	{}
};

接下来我们来分析,红黑树插入的各种情况。

首先,红黑树的插入的第一个问题就是我们需要插入的是什么颜色的节点,红色还是黑色?那么我们来分析一下:
如果插入红色节点:可能会破坏规则3,但是不会破坏规则4
如果插入黑色节点:必然破坏规则4。

而权衡规则3和规则4,规则4的维护成本非常大!因此我们选择插入的是红色节点!
接下来我来解释一下图示的符号:

g:表示祖父节点
u:表示叔叔节点
p:表示父亲节点
c:表示当前节点

cur就是新增的节点:
而红黑树的插入的调整方式最关键的就是看叔叔的情况。
情况1;叔叔存在且是红色
那么这时候这种情况下,假设说我们此时新增一个节点cur如下图:
在这里插入图片描述
那么我们处理的方式就是:pa和uncle节点变成黑色,然后把祖父节点变成红色,然后我们需要继续向上调整处理!
在这里插入图片描述
那么cur节点是红色节点的情况还有可能是由于情况1的调整过来的!但是我们的处理方式也是和前面是相同的!
那么这是叔叔节点存在并且叔叔节点是红色节点的情况。

而插入情况里面,最为复杂的就是叔叔节点不存在或者叔叔节点存在且叔叔节点是黑色节点的情况,这种情况的插入就是相对复杂。下面我们就来分析一下

左单旋

情况2:叔叔节点不存在或者叔叔节点存在且为黑色
由于规则里面规定了空节点的颜色是黑色,所以情况2可以概括成为叔叔节点为黑色。那么对于这种情况,cur节点是红色必然只有可能是由情况1变化而来!(否则先前就不是红黑树).那么我们来看如下的这种情况:
在这里插入图片描述
而这种情况我们就需要进行旋转处理:以祖父为轴进行一次左单旋,然后祖父变红,父亲变黑,而这个时候因为父亲节点就变成祖父节点,此时不需要再继续向上调整了。
在这里插入图片描述

右单旋

而如果是下面这种情况,那么就要进行右单旋进行调整:
在这里插入图片描述
那么进行右单旋以后,对g,p进行变色即可:
在这里插入图片描述

右左双旋

前面两种情况都是一次旋转就可以解决的。回想我们先前AVL树插入会出现需要双旋的情况。红黑树也会存在需要进行双旋转的情况。我们来看下面这种情况:
在这里插入图片描述
处理的方式:以父亲为轴进行右单旋,再以祖父节点为轴进行左单旋转
在这里插入图片描述

左右双旋

和上面的情况类似,下面的情况需要进行的就是左右双旋:
在这里插入图片描述

代码实现

下面就是对前面描述的各种情况的代码描述:

bool insert(const pair<K, V>& kv)
	{   
		if (!_root)
		{  
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{   //左子树
			if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			//右子树
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			//停止重复插入
			else
			{
				return false;
			}
		}
		//插入节点默认是红色
		cur = new Node(kv);
		if (parent->_kv.first > kv.first)
			parent->_left = cur;
		else
			parent->_right = cur;
		cur->_col = RED;
		cur->_parent = parent;
		//如果父亲存在且为红色,违反规则三,需要处理
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				//情况1:叔叔存在且为红色
				//处理方式:叔叔和父亲变黑,祖父变红,继续向上调整
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				//叔叔不存在||叔叔存在且是黑色
				else
				{
					//如果父亲是祖父的左且cur也是父亲的左要右单旋
					if (cur == parent->_left)
					{
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					//如果cur是父亲的右,就要双旋
					/*   g
					*   p
					*    c
					*/
					else
					{
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;						
					}
					break;
				}
			}
			//父亲是祖父的右
			else
			{
				Node* uncle = grandfather->_left;
				//情况1:叔叔存在且为红色
				//处理方式:叔叔和父亲变黑,祖父变红,继续向上调整
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					cur = grandfather;
					parent = cur->_parent;
				}
				//叔叔不存在||叔叔存在且是黑色
				else
				{  
					
					//如果父亲是祖父的右且cur也是祖父的右要左单旋转
					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;
	}
	
		
private:
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* ppNode = parent->_parent;
		parent->_left = subLR;
		subL->_right = parent;
		parent->_parent = subL;
		if (subLR)
			subLR->_parent = parent;
		if (parent == _root)
		{
			_root = subL;
			_root->_parent  = nullptr;
		}
		else
		{
			
			if (parent == ppNode->_left)
				ppNode->_left = subL;
			else
				ppNode->_right = subL;
			subL->_parent = ppNode;
		}
		
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* ppNode = parent->_parent;
		parent->_right = subRL;
		parent->_parent = subR;
		subR->_left = parent;
		if (subRL)
			subRL->_parent = parent;
		if (_root == parent)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (parent == ppNode->_left)
				ppNode->_left = subR;
			else
				ppNode->_right = subR;
			subR->_parent = ppNode;
		}
	}

和AVL树进行对比

由于红黑树的删除相对比较复杂,后面会单独抽出一篇博客专门讲解红黑树的删除(AVL树的删除也会单独写一篇博客),接下来我们就来对比一下AVL树和红黑树。

1.AVL树是严格的平衡,查询效率是O(logn),而红黑树是近似的平衡,不过对于计算机来说,也可以近似认为是logn

2.AVL树插入的时候会频繁进行旋转,性能消耗大
3.红黑树实际实现相对简单

红黑树的应用

  1. STL里的set和map的底层数据结构
  2. linux的epoll
  3. java标准库的TreeMap底层容器

以上就是本文的主要内容,如果有不足之处还望指出。

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值