红黑树详解

一.红黑树介绍

1.红黑树的概念

红黑树是一颗二叉搜索树,增加了一个储存位来表示颜色,可以是红色或者黑色。通过对从根到叶子(指的是nullptr节点)的路径上每个节点颜色的约束,红黑树确保了没有一条路径会超出其他路径长度的两倍,因而接近平衡。

2.红黑树的规则

每条路径的结尾都是NIL节点(传统的叶节点,而是空节点,为了方便准确标识所有路径)

1.红黑树节点不是黑色就是红色;

2.整棵树的根节点一定是黑色;

3.如果一个节点是红色的,那么它的两个孩子必然是黑色的,也就是说,在红黑树的任意路径上没有连续的两个红节点;

4.对于任意节点,从该节点到其所有NULL节点 的路径上,黑节点的数量相同。

注意:有些树上会补充NIL节点都是黑色的规则。后面讲解的细节中会忽略NIL。

为什么红黑树确保了没有一条路径会超出其他路径长度的两倍?

1.由规则3可得,最短路径(极端情况)的节点全是黑节点,设其高度为bh(black height);

2.为了让路径更长那么就得让路径上的红节点多一点,但不允许存在连续的红节点,那么路径最长时(极端情况)就是每一个黑节点接一个红节点,路径长为2 * bh;

3.综合而言,设红黑树的每条路径的长度为x,bh<= x <= 2 * bh;

3.红黑树的效率

设红黑树的节点个数为N,最短路径长为h,那么有2^h - 1 <= N < 2^(2 * h) - 1,所以h约等于logN,也就是说增删查改最多要走2*logN,故其效率是O(logN)。

相比于AVL树:

红黑树的表达相对于AVL树要抽象一点,AVL树通过对高度差的观察控制了平衡;而红黑树是通过四条规则约束,间接实现了近似平衡。它们的效率是同一档次的,但插入相同的数据时红黑树的旋转次数要少一点,因为它对平衡的控制相对轻松,而旋转消耗比较大,所以实际应用中红黑树是使用更多的。

二.红黑树的实现

1.红黑树的结构
cpp 复制代码
enum Color
{
	RED,
	BLACK
};
template<class k,class v>
struct RBTreeNode
{
	typedef RBTreeNode<k,v> Node;
	RBTreeNode(const pair<k,v>& data = pair<k,v>())
		:_kv(data)
		,_left(nullptr)
		,_parent(nullptr)
		,_right(nullptr)
		,_col(BLACK)
	{ }
	pair<k, v> _kv;
	Node* _parent;
	Node* _left;
	Node* _right;
	Color _col;
};
template<class k, class v>
class RBTree
{
	typedef RBTreeNode<k, v> Node;
public:
private:
	Node* _root = nullptr;
};
2.红黑树的插入
1.插入的大概过程

1.按照二叉搜索树的插入规则,插入后我们只需要观察它是否符合红黑树的四大规则;

2.如果是空树插入 ,那么插入的节点为根节点,且颜色为black ;如果是非空树插入,那么插入的节点必须为red ,因为插入black节点后会破坏规则4,它是很难维护的;

3.非空树插入新节点后,新节点为红色,如果其parent节点为black,则没有违反规则,插入结束;

4.非空树插入新节点后,新节点为红色,如果其parent节点为red,则会违反规则3,需要进一步分析。
重要节点说明:新插入的节点为cur( c ),其父节点为parent( p ),祖父节点为grandparent( g ),叔节点为uncle( u )。

新插入节点后,如果要进一步分析,那么四个重要节点中会有三个节点的颜色是知道的:

cur和parent都是red(它们导致要进一步分析),grandparent为black,只有uncle未知。以下的分析就是围绕uncle的不同来分类的

设bh为每条路径黑节点的数量

2.情况一:变色

uncle为红色:

分析:p、u都是红色,此时把它们变黑,g变红,这样解决了p、c连续红节点的问题,也没有改变以g为根的两条路径的bh,使得以g为根的子树符合红黑树的规则。但要注意的是,g变红了,如果g的parent是黑色,那么插入结束;如果是红色,那么就要让g变成c继续向上更新。

抽象图理解:这里只展示了p为g的左孩子的情况,插入右子树也差不多。

一直更新到g为根节点或它的oarent为black为止。
单变色要的条件

uncle存在且为red;

cpp 复制代码
bool insert(const pair<k, v> data)
{
	//先普通的插入
	if (!_root)
	{
		_root = new Node(data);
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (data.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (data.first > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
			return false;
	}
	cur = new Node(data);
	cur->_col = RED;
	cur->_parent = parent;
	if (data.first < parent->_kv.first)
		parent->_left = cur;
	else
		parent->_right = cur;

	//调整节点的col
	while (parent && parent->_col == RED)
	//更新到cur为根节点会自动跳出
	{
		Node* grandparent = parent->_parent;
		if (parent == grandparent->_left)//cur插入在左子树的情况
		{
			Node* uncle = grandparent->_right;
			if (uncle && uncle->_col == RED)//情况1
			//uncle为红的情况,将parent和uncle置为黑,gp置为红
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandparent->_col = RED;
				cur = grandparent;
				parent = cur->_parent;
			}
		}
		else//cur插入在右子树的情况
		{
			Node* uncle = grandparent->_left;
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandparent->_col = RED;
				cur = grandparent;
				parent = cur->_parent;
			}
		}
	}
	_root->_col = BLACK;//保存根节点为黑
	return true;
}
3.情况2:单旋+变色

当uncle为空或其_col为black时,单靠变色时无法解决问题的,还得加上旋转。
当u不存在时,cur一定时新插入的节点

如图假设u不存在时,cur不是新插入节点,因为都更新到这一步了,ABC肯定是存在黑节点的,但g的右子树没有黑节点,所以这种情况不存在,u为空,cur只能是新插入的节点

当u存在且颜色为黑时,cur一定不是新插入节点

假设cur为新插入节点:

明显可见,在插入cur前,以g为根的树的左右子树黑接待你个数就不同,所以u为黑,cur必不可能是新插入节点

抽象图分析:这里分析p为g的右孩子。

可见,如果p是g的右孩子,cur是p的右孩子,只需要以g为旋点左旋,最后

p:red->black

g:black->red

即可。
单旋 + 变色要的条件:

1.uncle不存在或存在且为black;

2.当p为g的左/右孩子时,cur为p的左/右孩子。

cpp 复制代码
while (parent && parent->_col == RED)
{
	Node* grandparent = parent->_parent;
	if (parent == grandparent->_left)
	{
		Node* uncle = grandparent->_right;
		if (uncle && uncle->_col == RED)
		//uncle为红的情况,将parent和uncle置为黑,gp置为红
		{
			parent->_col = BLACK;
			uncle->_col = BLACK;
			grandparent->_col = RED;
			cur = grandparent;
			parent = cur->_parent;
		}
		else if (uncle == nullptr || uncle->_col == BLACK)
		{
			if (cur == parent->_left)//单旋接变色
			//情况2
			{
				//   g
				// p   u
				//c
				RotateR(grandparent);
				parent->_col = BLACK;
				grandparent->_col = RED;
			}
			break;
		}
	}
	else//p == gp->r
	{
		Node* uncle = grandparent->_left;
		if (uncle && uncle->_col == RED)
		{
			parent->_col = BLACK;
			uncle->_col = BLACK;
			grandparent->_col = RED;
			cur = grandparent;
			parent = cur->_parent;
		}
		else if (uncle == nullptr || uncle->_col == BLACK)
		{
			if (cur == parent->_right)//单旋接变色
			{
				//   g
				// u   p
				//		 c
				RotateL(grandparent);
				parent->_col = BLACK;
				grandparent->_col = RED;
			}
			break;
		}
	}
}

//左右单旋:
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* Pparent = parent->_parent;

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

		if (parent == _root)
			_root = subL;
		subL->_right = parent;
		subL->_parent = Pparent;

		if (Pparent && Pparent->_left == parent)
			Pparent->_left = subL;
		else if(Pparent)
			Pparent->_right = subL;
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* Pparent = parent->_parent;

		parent->_right = subRL;
		parent->_parent = subR;
		if (subRL)
			subRL->_parent = parent;

		if (parent == _root)
			_root = subR;
		subR->_left = parent;
		subR->_parent = Pparent;

		if (Pparent && Pparent->_left == parent)
			Pparent->_left = subR;
		else if(Pparent)
			Pparent->_right = subR;
	}
4.情况3:双旋+变色

抽象图分析:p为g的右孩子。

可见当p为g的右孩子,cur为p的左孩子时,得先以p为旋点右旋,在以p为旋点左旋,最后进行变色处理:

g:black->red;

c:red->black;
双旋 + 变色的条件

1.uncle不存在/存在且为黑;

2.p为g的右/左孩子时,c为p的左/右孩子;

cpp 复制代码
while (parent && parent->_col == RED)
{
	Node* grandparent = parent->_parent;
	if (parent == grandparent->_left)
	{
		Node* uncle = grandparent->_right;
		if (uncle && uncle->_col == RED)
		//uncle为红的情况,将parent和uncle置为黑,gp置为红
		{
			parent->_col = BLACK;
			uncle->_col = BLACK;
			grandparent->_col = RED;
			cur = grandparent;
			parent = cur->_parent;
		}
		else if (uncle == nullptr || uncle->_col == BLACK)
		{
			if (cur == parent->_left)//单旋接变色
			{
				//   g
				// p   u
				//c
				RotateR(grandparent);
				parent->_col = BLACK;
				grandparent->_col = RED;
			}
			else//双旋接变色
			{
				//   g
				// p   u
				//  c
				RotateL(parent);
				RotateR(grandparent);
				cur->_col = BLACK;
				grandparent->_col = RED;
			}
			break;
		}
	}
	else//p == gp->r
	{
		Node* uncle = grandparent->_left;
		if (uncle && uncle->_col == RED)
		{
			parent->_col = BLACK;
			uncle->_col = BLACK;
			grandparent->_col = RED;
			cur = grandparent;
			parent = cur->_parent;
		}
		else if (uncle == nullptr || uncle->_col == BLACK)
		{
			if (cur == parent->_right)//单旋接变色
			{
				//   g
				// u   p
				//		 c
				RotateL(grandparent);
				parent->_col = BLACK;
				grandparent->_col = RED;
			}
			else//双旋接变色
			{
				//   g
				// u   p
				//    c
				RotateR(parent);
				RotateL(grandparent);
				cur->_col = BLACK;
				grandparent->_col = RED;
			}
			break;
		}
	}
}
5.插入的完整代码
cpp 复制代码
bool insert(const pair<k, v> data)
{
	//先普通的插入
	if (!_root)
	{
		_root = new Node(data);
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (data.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (data.first > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
			return false;
	}
	cur = new Node(data);
	cur->_col = RED;
	cur->_parent = parent;
	if (data.first < parent->_kv.first)
		parent->_left = cur;
	else
		parent->_right = cur;

	//调整节点的col
	while (parent && parent->_col == RED)
	{
		Node* grandparent = parent->_parent;
		if (parent == grandparent->_left)
		{
			Node* uncle = grandparent->_right;
			if (uncle && uncle->_col == RED)
			//uncle为红的情况,将parent和uncle置为黑,gp置为红
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandparent->_col = RED;
				cur = grandparent;
				parent = cur->_parent;
			}
			else if (uncle == nullptr || uncle->_col == BLACK)
			{
				if (cur == parent->_left)//单旋接变色
				{
					//   g
					// p   u
					//c
					RotateR(grandparent);
					parent->_col = BLACK;
					grandparent->_col = RED;
				}
				else//双旋接变色
				{
					//   g
					// p   u
					//  c
					RotateL(parent);
					RotateR(grandparent);
					cur->_col = BLACK;
					grandparent->_col = RED;
				}
				break;
			}
		}
		else//p == gp->r
		{
			Node* uncle = grandparent->_left;
			if (uncle && uncle->_col == RED)
			{
				parent->_col = BLACK;
				uncle->_col = BLACK;
				grandparent->_col = RED;
				cur = grandparent;
				parent = cur->_parent;
			}
			else if (uncle == nullptr || uncle->_col == BLACK)
			{
				if (cur == parent->_right)//单旋接变色
				{
					//   g
					// u   p
					//		 c
					RotateL(grandparent);
					parent->_col = BLACK;
					grandparent->_col = RED;
				}
				else//双旋接变色
				{
					//   g
					// u   p
					//    c
					RotateR(parent);
					RotateL(grandparent);
					cur->_col = BLACK;
					grandparent->_col = RED;
				}
				break;
			}
		}
	}
	_root->_col = BLACK;//保存根节点为黑
	return true;
}

在插入过程中,红黑树靠什么增加bh大小的?

单看情况1、2、3每执行一次bh的大小时不变的。在插入过程中,其大小的变化只会由一种情况引起,那就是在变色环节:while (parent && parent->_col == RED) 中一直更新到根节点,且当g为根节点时,u为red,这是会按情况1执行,让根变为red后跳出循环,再在循环外让根变黑,这时bh就增加了1。

三.红黑树的检验

判断一棵树是否为红黑树只要看它是否满足红黑树的四条规则就好了:

cpp 复制代码
template<class k, class v>
class RBTree
{
	typedef RBTreeNode<k, v> Node;
public:
	bool isbalance()
	{
		if (_root == nullptr)
			return true;
		if (_root->_col == RED)
			return false;
		int way = 0;
		Node* cur = _root;
		while (cur)//获取单条路径的bh
		{
			if (cur->_col == BLACK)
				++way;
			cur = cur->_left;
		}
		return check(_root, 0, way);
	}
		bool check(Node* root, int blacknum, const int refnum)
		//refnum为每条路径的黑节点个数
	{
		if (root == nullptr)
		{
			if (blacknum == refnum)//代码走到着时就说明一条路走完了
				return true;
			else
			{
				cout << "有路径的黑色节点数不相等"<<endl;
				return false;
			}
		}
		if (root->_col == BLACK)
			blacknum++;
		else//r->c = red
		//检查子树不太方便,因为有两个,而且还不一定存在,所以检查parent
		{
			if (root->_parent && root->_parent->_col == RED)
				return false;
		}
		return check(root->_left, blacknum, refnum)
			&& check(root->_right, blacknum, refnum);
	}
相关推荐
HZ·湘怡1 小时前
基于动态数组的栈(顺序栈)01
数据结构·算法
kyle~1 小时前
RTPS(Real-Time Publish-Subscribe)---DDS的传输协议
c++·机器人·ros2
雁迟1 小时前
第九章:列表 List 数据类型
数据结构·r语言
TIEM_691 小时前
C++ vector容器全面解析:从入门到精通
开发语言·c++
Irissgwe1 小时前
c++多态
开发语言·c++·多态
m0_547486661 小时前
郑州轻工业大学《数据结构》期末试卷及答案2018-2022学年(AB卷)
数据结构·期末试卷·郑州轻工业大学
lingran__1 小时前
C++_类和对象(上)
开发语言·c++
变量未定义~2 小时前
字符串哈希匹配字符串
数据结构·算法·哈希算法
lzh200409192 小时前
手搓一个简易 Linux 进程池:巩固进程知识
linux·c++