[数据结构]——浅谈红黑树原理与简易实现

红黑树

我之前的博客讲解了AVL树的性质,通过对AVL树的了解我们知道了他是一颗高度平衡的二叉搜索树,其实二叉搜索树最大的作用就是进行插入,删除,查找的操作,而AVL树查找的时间复杂度为log(n)。为了保持平衡的性质(左右子树的高度差绝对值不大于一),AVL在进行插入数据时就要进行大量的旋转,当我们的数据量非常的大时,其实我们在进行插入调整这颗树的旋转操作也会有很大的开销,这里就出现了一颗叫红黑树的东西。

红黑树也是一颗二叉搜索树,他的节点不使用平衡因子来控制树的形状,而是采用了节点的两种颜色来控制使得树中最长路径中节点个数不会超过最短路径节点个数的两倍,这样就能保证树触发旋转的几率大大减小。但是有人会说红黑树的效率会不会变低,答案当然是会的,AVL树的查找时间复杂度为log(n),红黑树的查找时间复杂度为nlog(n),但是,对于计算机的运算速度是惊人每秒的运算量都是亿级的,所以在他的眼里log(n)和nlog(n)事实上没有什么区别。

正如我的老师所说,发明红黑树的人真是个奇才,他能从问题不同的角度切入,为这个世界创造了更多的可能性,值得我们每一个人学习。

红黑树的性质

首先一起来看看红黑树的性质:

  • 每个结点不是红色就是黑色(emmm…,彷佛在说废话)
  • 根节点是黑色的
  • 如果一个节点是红色的,则它的两个孩子结点是黑色的 (没有连续的红色节点)
  • 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点 (到空节点的路径上黑色节点数量相同)
  • 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

只要我们可以保证上面的规则,就能保证我们前面所说的,树中最长路径中节点个数不会超过最短路径节点个数的两倍,但是这是为什么呢?

  • 最短路径:一定是纯黑色的
  • 最长路径:一定是红黑相间(这里要保证上面的第四条性质)
    在这里插入图片描述

红黑树的插入

说了这么多,其实我们这颗树就是使用红色和黑色这两种状态来控制树的形状的,但是具体的怎么控制形状,需要我们进行深入的思考。

首先根据我们的第一条性质,我们红黑树的根节点必为黑色,所以当我们这颗树是一颗空树时,直接返回颜色为黑的根节点,但是紧接着问题来了,我们之后插入节点是该插入红色的节点还是黑色的节点呢?其实我们前面提到过,如果对于新的节点插入颜色为黑的节点,此时我们就不能保证所有路径上黑节点数量相同,如果硬将其插入黑色节点,那么必须对所有路径进行调整,那么将会付出很大的代价。

所以我们在进行新插入节点时,都插入红色节点。插入后,如果父节点为黑就结束,如果父节点为红那么就要对树节点的颜色和形状进行调整。下面的讲解中不同的标识分别代表:

  • cur:新插入节点
  • parent:新插入节点的父亲
  • garndfather:新插入节点的父亲的父亲
  • uncle:新插入节点的父亲的兄弟

1.新插入节点的父亲为红,叔叔存在且颜色为红

下图中是我们遇到的第一种最简单的情况,新插入节点的父亲为红色,叔叔节点的颜色为红,我们就要进行如下操作:

  1. 将cur的parent节点和叔叔(uncle)节点调整为黑色
  2. 将grandfather节点调整为红色在这里插入图片描述

但是这里我们只讲了树结构中的一部分,如果grandfather的parent依旧是红色的节点我们就需要接着向上调整,如果grandfather的parent为空说明grandfather就是root,所以将他的颜色变为黑色并且结束。
在这里插入图片描述

2. 新插入节点父亲为红,叔叔节点不存在或存在且为黑(单旋)

下图中是我们遇到的第二种情况,新插入节点的父亲为红色,叔叔节点的颜色为黑或者不存在。

注意: 我们现在下图的遇到的这种情况一定是发生在上面我们讲的第一种情况调整之后,从图中我们可以看出,cur绝对不可能是新插入的节点,因为,如果cur是新插入节点这颗树就不满足各路径黑色节点相同的性质,所以cur之前肯定为黑色,是后来被调整为红色。
在这里插入图片描述
当我们遇到这种情况时就需要对树以grandfather为轴节点进行旋转:

  1. 让parent的右子树变为grandfather的左子树,grandfather变成parent的右子树
  2. 将parent颜色变为黑色,grandfather变为红色

在这里插入图片描述
当然了,结合情况一就会出现下面的情况:这也就清楚的解释了为什么情况二出现在情况一的基础上
在这里插入图片描述

3. 新插入节点父亲为红,叔叔节点不存在或存在且为黑(双旋)

其实第三种与第二种情况基本相同,只不过这时我们需要进行俩次旋转才能达到目的:
在这里插入图片描述

此时这种情况时就需要对树先以parent为轴节点进行左旋,然后以grandfather为轴节点进行右旋:需要注意的是,在进行第一次左旋后节点的颜色不改变,而在第二次的右旋之后需要进行将父亲和grandfather进行颜色调整。
在这里插入图片描述
所以更复杂的树就有了下面的场景:
在这里插入图片描述
以上就是我们在调整红黑树时会遇到的不同情况,旋转仅仅拿出右单旋和左右双旋来演示,左单旋和右左双旋也是相同的道理,有了上面的讲解我想大家可以独立完成另外两种旋转情况,但是光有图还不够,我们来进一步剖析插入的代码。

代码看起来虽然长,但是根据思路梳理并不难理解(每个人代码风格不同,仅供参考)

bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}
		Node* parent = nullptr;//记录cur节点的上一个节点
		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);
		cur->_col = RED;//新节点插入红色节点
		if (parent->_kv.first > kv.first)//将cur插入树中
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_right = cur;
			cur->_parent = parent;
		}

		while (parent && parent->_col == RED)//如果当前插入节点的父亲节点为黑色则不需要调整
		{
			Node* grandfather = parent->_parent;//通过grandfather查看uncle节点在其的哪一侧
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)//如果叔叔为红色,对应情况1
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else//如果叔叔为黑色,或者不存在
				{
					if (cur == parent->_right)//判断是否需要进行双旋,对应情况3
					{
						RotateL(parent);
						swap(cur, parent);//对照图发现,需要交换指针
					}
					RotateR(grandfather);//只单旋对应情况二
					parent->_col = BLACK;
					grandfather->_col = RED;
					break;
				}
			}
			else//相反的情况
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_left)
					{
						RotateR(parent);
						swap(cur, parent);
					}
					RotateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
					break;
				}
			}
		}
		_root->_col = BLACK;//一定注意当情况一发生后,可能根节点是红色,每次插入后跟新成黑色
		return true;
	}

红黑树的删除

红黑树的删除有一定的难度,这里我们就不详细介绍了,其实它跟搜索树的删除很类似就是寻找一个替换节点然后删掉它的替换节点
在这里插入图片描述
删除分三个情况:

  • del的左为空.

  • del的右为空.

  • del的左右都不为空

因为这里红黑树的删除还没有搞定,这里就先不贴代码了。

红黑树的验证

红黑树的验证是个棘手的问题,但是根据我们前面的红黑树规则,可以对目标树是否是红黑树进行判断:

  1. 查看数的根节点是否为黑色
  2. 若当前节点是红色节点查看是否存在连续的红色节点
  3. 统计一条路径上的黑色节点并且作为基准,递归查看其他路径上的黑色节点是否等于基准值

这是我们用于判断构建的树是否为红黑树的代码:

bool ISRBtree()
	{
		if (_root->_col == RED)
		{
			return false;
		}
		size_t n = 0;//基准值
		size_t m = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				n++;//统计一条路径上的黑色节点个数
			}
			cur = cur->_left;
		}
		return _ISRBtree(_root, m, n);
	}

	bool _ISRBtree(Node* root, size_t m, size_t n)//递归对每一条路径上的值进行查看是否都相同
	{
		if (root == NULL)
		{
			if (m == n)
				return true;
			else
				return false;
		}

		if (root->_col == BLACK)
		{
			m++;
		}
		if (root->_col == RED && root->_parent && root->_parent->_col == RED)//判断是否存在连续的红色节点
		{
			return false;
		}
		return _ISRBtree(root->_left, m, n) && _ISRBtree(root->_right, m, n);
	}

测试用例:

16, 3, 7, 11, 9, 26, 18, 14, 15

验证结果:
在这里插入图片描述

完整代码

这里只将构建红黑树的代码贴出来(emmm,主要是太长了),其他代码读者可以自己尝试实现。

#include<iostream>
#include<windows.h>
using namespace std;

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>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	RBTree()
		:_root(nullptr)
	{}

	~RBTree()
	{}

	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}
		Node* parent = nullptr;//记录cur节点的上一个节点
		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);
		cur->_col = RED;//新节点插入红色节点
		if (parent->_kv.first > kv.first)//将cur插入树中
		{
			parent->_left = cur;
			cur->_parent = parent;
		}
		else
		{
			parent->_right = cur;
			cur->_parent = parent;
		}

		while (parent && parent->_col == RED)//如果当前插入节点的父亲节点为黑色则不需要调整
		{
			Node* grandfather = parent->_parent;//通过grandfather查看uncle节点在其的哪一侧
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)//如果叔叔为红色,对应情况1
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else//如果叔叔为黑色,或者不存在
				{
					if (cur == parent->_right)//判断是否需要进行双旋,对应情况3
					{
						RotateL(parent);
						swap(cur, parent);//对照图发现,需要交换指针
					}
					RotateR(grandfather);//只单旋对应情况二
					parent->_col = BLACK;
					grandfather->_col = RED;
					break;
				}
			}
			else//相反的情况
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					uncle->_col = parent->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (cur == parent->_left)
					{
						RotateR(parent);
						swap(cur, parent);
					}
					RotateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
					break;
				}
			}
		}
		_root->_col = BLACK;//一定注意当情况一发生后,可能根节点是红色,每次插入后跟新成黑色
		return true;
	}

	void RotateR(Node* parent)//右单旋
	{
		Node* SubL = parent->_left;
		Node* SubLR = SubL->_right;
		parent->_left = SubLR;
		if (SubLR)
		{
			SubLR->_parent = parent;
		}
		SubL->_right = parent;
		Node* pNode = parent->_parent;
		parent->_parent = SubL;
		if (parent == _root)
		{
			_root = SubL;
		}
		else
		{
			if (pNode->_left == parent)
			{
				pNode->_left = SubL;
			}
			else
			{
				pNode->_right = SubL;
			}
		}
		SubL->_parent = pNode;
	}

	void RotateL(Node* parent)//左单旋
	{
		Node* SubR = parent->_right;
		Node* SubRL = SubR->_left;
		parent->_right = SubRL;
		if (SubRL)
		{
			SubRL->_parent = parent;
		}
		SubR->_left = parent;
		Node* pNode = parent->_parent;//记录当前parent的父亲节点,以便让SubR指向当前parent的父亲节点
		parent->_parent = SubR;
		if (_root == parent)
		{
			_root = SubR;
		}
		else
		{
			if (pNode->_left == parent)
			{
				pNode->_left = SubR;
			}
			else
			{
				pNode->_right = SubR;
			}
		}
		SubR->_parent = pNode;
	}	

	void Inorder()
	{
		_Inorder(_root);
		cout << endl;
	}

	void _Inorder(Node* root)
	{
		if (root == nullptr)
			return;
		_Inorder(root->_left);
		cout << root->_kv.first << " ";
		_Inorder(root->_right);
	}

	bool ISRBtree()
	{
		if (_root->_col == RED)
		{
			return false;
		}
		size_t n = 0;
		size_t m = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				n++;
			}
			cur = cur->_left;
		}
		return _ISRBtree(_root, m, n);
	}

	bool _ISRBtree(Node* root, size_t m, size_t n)
	{
		if (root == NULL)
		{
			if (m == n)
				return true;
			else
				return false;
		}


		if (root->_col == BLACK)
		{
			m++;
		}
		if (root->_col == RED && root->_parent && root->_parent->_col == RED)
		{
			return false;
		}
		return _ISRBtree(root->_left, m, n) && _ISRBtree(root->_right, m, n);
	}


private:
	Node* _root;
};

总结

红黑树真的是一个非常重要的数据结构,并且他的实现极其的复杂,我们是因为看到了经过很多大师的总结所以才感觉理解起来不是那么困难,红黑树重在理解他的插入节点时不同情况怎么来对树的形状和节点颜色进行调整,只要理解了总的框架,也就变得更容易的实现,这里就是我对红黑树的浅见,如果有什么问题,还请大家多多指出。

  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值