红黑树的模拟实现

什么是红黑树?

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或
Black
。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制。

受下面的性质3和性质4的限制,红黑树确保没有一条路径会比其他路径长出两倍,因而是接近平衡的。

红黑树的性质:

1、每个结点不是红色就是黑色
2、树的根节点是黑色的
3、同一条路径中不能出现连续的红色结点
4、每条路径均包含相同数目的黑色结点

红黑树的实现:

结点的定义:

用枚举来限定结点的颜色是红色还是黑色,也可以用 true / false 来区分红黑色。

enum Color
{//枚举,限定红黑树结点的颜色
	BLACK,
	RED
};
template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	Color _col;

	RBTreeNode(const pair<K, V>& kv)
		:_right(nullptr)
		, _left(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _col(RED)//初始化为红色,方便插入
	{ }
};

插入 Insert:

假设我们已经找到要插入的位置了,我们设想下面几种情形:

1、插入时新增结点应该设置为什么颜色?

根据红黑树每条路径具有相等数量的黑色结点,如果设置为黑色,相当于我们主动破坏了游戏规则,红黑树中有某条路径的黑色节点的个数比其他路径多,这时我们很难再让红黑树的黑色结点的个数达到平衡,所以插入时新增结点的颜色设置为红色比较合适。

2、假设插入的结点为 cur,parent 为 cur 的父亲节点,grandfather 为parent 的父亲结点,uncle 是 grandfather 的另一个孩子

a.如果 parent 的颜色为黑色,因为我们没有破坏红黑树的游戏规则(没有出现连续的红色结点,且每条路径黑色节点的数量相等),这时候插入可以结束了

b.如果 parent 的颜色为红色 ,我们用简图来表示:

 由于 parent 为红色,则 grandfather 也为红色,否则插入前这棵红黑树已经存在连续的红色节点,已经不平衡了,此时 uncle 的颜色可能为红色,可能为黑色,我们可以根据 uncle 的颜色来讨论下一步怎么变色。

1、uncle存在且为红

由于出现连续的红色节点,我们需要 cur 和 parent 中选一个结点变色,但我们不能改变 cur 的颜色,如果把 cur 的颜色改为黑色,这和一开始直接插入黑色结点没有区别,所以我们把 parent 变为黑色,parent 改为黑色之后,每条路径黑色结点的数量不相等,所以把 grandfather 改为红色,uncle 改为黑色。

因为 grandfather 的颜色变为红色了,可能 grandfather 的父亲也是红色结点,又出现连续的红色结点,这也是我们需要考虑的问题,故需要继续向上更新,直到更新后的 parent 为空 或者 parent 的颜色为黑色,更新停止。

cur = grandfather;
parent = cur->_father;

2、uncle不存在或uncle存在且为黑

假设从上面的情况向上更新后,发现 parent 为红色,出现连续的红色结点,uncle 存在且为黑,如果和上面一样,把 parent 变为黑色,grandfather 变为红色,此时每条路径黑色结点的数量不相等了,原本 grandfather 的右子树可能有2个黑色节点,现在变为 1 个了,所以仅靠变色已经无法使红黑树平衡了,需要先旋转,再变色,使红黑树再次平衡。(和 AVL 的旋转规则一样,对旋转规则不熟悉的读者,可以看上一篇 AVL 的实现)。

此时我们需要分类讨论:

a. 如果 uncle 为 grandfather 的右孩子,且 cur 为 parent 的左孩子

b. 如果 uncle 为 grandfather 的右孩子,且 cur 为 parent 的右孩子

c. 如果 uncle 为 grandfather 的左孩子,且 cur 为 parent 的左孩子

d. 如果 uncle 为 grandfather 的左孩子,且 cur 为 parent 的右孩子

uncle 不存在和 uncle 存在且为黑 是一样的处理方式。

变色完之后,已经不需要向上更新了,因为我们当前处理的子树的根已经变为黑色了,向上更新得到的子树也一样符合红黑树的规则,即整棵红黑树已经平衡了,插入可以结束了。

插入的最后,把红黑树的根变为黑色,防止在插入的变色过程中对根的颜色进行修改。 

    void RotateL(Node* parent)
	{//左单旋,要转下来的结点作为parent
		Node* subR = parent->_right;//父亲节点的右
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)//避免空指针的解引用
			subRL->_parent = parent;

		subR->_left = parent;
		Node* ppnode = parent->_parent;
		parent->_parent = subR;

		if (parent == _root)
		{//如果转下来的结点是根
			_root = subR;
			subR->_parent = nullptr;
		}
		else
		{//如果转下来的结点不是根
			if (ppnode->_left == parent)//parent在ppnode的左边
			{
				ppnode->_left = subR;
			}
			else//parent在ppnode的右边
			{
				ppnode->_right = subR;
			}
			subR->_parent = ppnode;
		}
	}
	void RotateR(Node* parent)
	{//右单旋,要转下来的结点作为parent
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		subL->_right = parent;
		Node* ppnode = parent->_parent;//先设好ppnode,因为下一句会修改parent的父亲节点,导致ppnode不是我们预想的
		parent->_parent = subL;

		if (parent == _root)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subL;
			}
			else
			{
				ppnode->_right = subL;
			}
			subL->_parent = ppnode;
		}
	}
	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->_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->_parent = parent;

		while (parent && parent->_col == RED)
		{//当parent不为空,且为红色时进入while
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)
			{//父亲在爷爷的左
				Node* uncle = grandfather->_right;//叔叔在爷爷的右
				if (uncle && uncle->_col == RED)
				{
					//情况一:叔叔存在且为红
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					//此时该子树的根grandfather的颜色为红,
					//grandfather的上半部分可能会出现连续的红色结点,故需要向上更新
					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					//叔叔不存在或叔叔存在且为黑
					if (cur == parent->_left)
					{
						//    g           p
						//  p   u -->  c    g
						//c                    u
						//g右旋
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;//该子树的根p 的颜色变为黑
					}
					else
					{
						//    g			c
						//  p   u --> p    g
						//   c              u
						//左右双旋
						RotateL(parent);
						RotateR(grandfather);
						grandfather->_col = RED;
						cur->_col = BLACK;//该子树的根cur 的颜色变为黑
					}
					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)
					{
						//   g				c
						//u		p -->     g		p
						//	  c			u
						//右左双旋
						RotateR(parent);
						RotateL(grandfather);
						grandfather->_col = RED;
						cur->_col = BLACK;
					}
					else
					{
						//	g			p
						//u   p -->	  g    c
						//		c	u
						//左单旋
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		_root->_col = BLACK;
		return true;
	}

判断红黑树是否平衡 IsBalance:

可以根据红黑树的性质,判断红黑树是否平衡:

1、红黑树的根为黑色:直接 if 判断根节点的颜色是否为黑色,判断之前,一定一定要先判断根是否存在,否则会出现空指针的解引用。

2、红黑树不存在连续的红色结点:假设 cur 结点为红色,如果通过判断 cur 结点的孩子的颜色,来判断是否存在连续的红色结点,是比较麻烦的,因为 cur  可能有 2 个孩子,是一对二的情况,反过来,如果直接根据 cur 和 cur 的父亲节点的颜色来判断,是比较容易的,因为 cur 只有 1个父亲节点。

3、每条路径的黑色结点的数量相等:我们可以先算出红黑树的最左边 / 最右边 的路径的黑色结点的数量 refblacknum,把它作为基准值,再去计算其他路径的黑色结点的个数,如果存在某条路径的黑色结点的个数和 refblacknum 不相等,返回 false,如果每条路径的黑色结点的数量都相等,返回 true。

    bool Check(Node* cur, int blacknum, int refblacknum)
	{
		if (cur == nullptr)
		{
            //一条路径已经走完了
			if (blacknum != refblacknum)
			{//当前路径黑色结点的个数和基准值不相等
				cout << "黑色节点数量不同" << endl;
				return false;
			}
			return true;//当前路径黑色结点的个数和基准值相等
		}
		if (cur->_col == RED && cur->_parent->_col == RED)
		{//出现连续的红色结点
			cout << cur->_kv.first << "出现连续的红色结点" << endl;
			return false;
		}
		if (cur->_col == BLACK)
		{
			blacknum++;
		}

		return Check(cur->_left, blacknum, refblacknum)
			&& Check(cur->_right, blacknum, refblacknum);
	}
	bool IsBalance()
	{
		//检查根是否为黑色,记得先检查根是否存在,否则会出现空指针的解引用
		if (_root && _root->_col == RED)
		{
			return false;
		}
		int refblacknum = 0;
		Node* cur = _root;
		while (cur)
		{
            //先计算最左路径的黑色结点,作为基准值
			if (cur->_col == BLACK)
				refblacknum++;

			cur = cur->_left;
		}
		return Check(_root, 0, refblacknum);
	}

  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值