初识C++之红黑树

目录

一、红黑树的概念

二、红黑树的性质

三、红黑树的平衡情况

四、红黑树的模拟实现

1. uncle存在且为红

2. u不存在/存在且为黑->单旋

2.1 p节点在g节点的左侧,c节点在p节点的左侧—左左->右单旋

2.2 p节点在g节点的右侧,c节点在p节点的右侧—右右->左单旋

 2.3 单旋总结

3. u不存在/存在且为黑->双旋

3.1 p节点在g节点的左侧,c节点在p节点的右侧——左右->左右双旋

 3.2 p节点在g节点的右侧,c节点在p节点的左侧——右左->右左双旋

3.3 双旋总结

5.红黑树调整总结

6.红黑树数据插入模拟实现


一、红黑树的概念

红黑树其实也是一种二叉搜索树。与普通的二叉搜索树相比,它每个节点上都新增了一个存储位表示结点的颜色,可以是Red或Black。通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长处两倍,因而是接近平衡的。

有人可能会疑惑,AVL树也是一种二叉搜索树,它能够保持高度平衡使得搜索效率为O(longN)。既然已经有AVL树了,那为什么还会有红黑树呢?从描述上看,它的搜索效率甚至可能比AVL树低一点。

其实是因为AVL树为了保持高度平衡,是通过旋转达到的。而每次旋转都会进行节点的调整。如果在数据量足够庞大的情况下,AVL树需要的调整次数也会随之增长。而红黑树则不同,红黑树对平衡要求并没有那么严格,仅仅只是要求最长路径不超过最短路径的两倍,这就导致红黑树在插入时需要的旋转次数会比AVL树少很多。假设现在有10亿数据,AVL树在插入时可能需要进行5000w次旋转,搜索30次;而红黑树则可能只需要2500w次旋转,搜索60次。从CPU的角度来看,30次和60次所消耗的时间差几乎可以忽略不计。但是在插入时红黑树的效率就比AVL树高很多。

因此,在现实中,如果需要使用到二叉搜索树的场景,一般都是优先使用红黑树,AVL树已经很少使用了,绝大部分情况下被红黑树所替代。

二、红黑树的性质

一棵红黑树,通常具有如下5个性质。

(1)每个结点不是红色就是黑色

(2)根节点时黑色的

(3)如果一个结点时红色的,则它的两个孩子节点是黑色的。(注意,这里只要求红色节点的两个子结点要为黑色,并没有要求黑色节点的子结点要为红色,所以黑色结点的子结点也可能是黑色)

(4)对于每个结点,从该结点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点

(5)每个叶子结点都是黑色的(此处的叶子节点指的是空节点)

初识C++之红黑树_第1张图片

上图就是一个红黑树。它的每条路径上的黑色结点都是相同的。

现在我们知道红黑树的最长路径不超过最短路径2倍。那么红黑树是如何做到这一点的呢?其实就是依托于性质3和性质4。

首先,性质3就规定了在一个红黑树中,不存在连续的红色节点。而性质4又规定每条路径上都有相同数目的黑色节点。通过这两个性质就可以知道,在一条路径上,红色结点的数量必然小于等于黑色结点。因此可以推断红黑树的最短路径就是全黑结点或者说是红色结点最少的路径;而最长路径就是红色和黑色结点相间的路径。

三、红黑树的平衡情况

与AVL树的高度平衡不同,红黑树是一种近似平衡。

红黑树的最优情况就是“左右平衡”。即路径全黑每条路径都是红黑相间。而最坏情况就是“左右高度不平衡”,即一边全黑且另一边黑红相间

四、红黑树的模拟实现

红黑树在插入时,势必会遇到需要进行调整的情况。而需要进行调整的情况,又分为三种。

在这里,首先假定插入的节点为curcur的父节点为parentparent父节点为groundfather节点parent的相邻节点为uncle节点

分别简写为c、p、g、u节点 :

初识C++之红黑树_第2张图片

 我们还要有一个认知,那就是在红黑树中,cur节点默认为红色。这就说明,如果需要调整,那么cur的parent节点必为红;又因为红黑树中不能有连续的红色节点,所以parent的父节点即g节点必为黑

因此,cur的p节点和g节点颜色在需要调整时都是固定的。唯一的变数就在于uncle节点。所以,以下所有情况都是针对于uncle节点及cur节点在p节点的位置来分类的。

1. uncle存在且为红

这种情况的红黑树如下图:

初识C++之红黑树_第3张图片

在这种情况下,要进行调整很简单,就是将p和u节点设置为黑,并将g节点设置为红即可。如果g为根节点,则重新设置为黑

初识C++之红黑树_第4张图片

要将g分为两种情况的原因还是,当g为根节点时,g节点默认为黑色,此时直接设置为黑色即可。但如果g不是根节点,那么该子树就可能需要以g为子节点继续向上调整。如以下情况:

初识C++之红黑树_第5张图片

在上图中,g就不是根节点,在调整完后,需要以g为子节点继续向上调整。向上调整时可能遇到其他情况,例如u为黑或u不存在等。这里演示的情况只是最简单的情况。

2. u不存在/存在且为黑->单旋

和AVL树一样,当插入节点时,如果p节点和c节点都在同一侧,则进行单旋。这就会分成两种情况

2.1 p节点在g节点的左侧,c节点在p节点的左侧—左左->右单旋

首先看u不存在的情况:

初识C++之红黑树_第6张图片

如果大家学习了AVL树,就会发现这种情况其实就和AVL树的右单旋情况很像。在这里,要调整红黑树,就可以以g节点为基点,进行一次右单旋。让g节点的左子节点指向p节点,p节点的右子节点指向g节点,然后更新对应节点的指针,并让g节点的原父节点指向p节点。

在节点调整完后,因为p节点此时代替g,颜色先修改为黑; 而g节点成为p节点的子节点,颜色修改为红色

初识C++之红黑树_第7张图片

 再来看u存在且为黑的情况:

这种情况一般原生不存在,而是由情况一,即u存在且为红时进行调整而来。

初识C++之红黑树_第8张图片

此时该树就违反了规则3——不能出现连续的红色节点。

要调整这棵树也很简单,同样的,以g为基点进行右单旋。让p的左子节点指向g,g的左子节点指向p的右子节点,然后分别调整p、g和p的原右子节点的父指针,并让g的原父节点指向p。

当旋转完后,将g的颜色修改为红,p的颜色修改为黑

初识C++之红黑树_第9张图片

通过如上方式调整后,就可以将树重新调整为红黑树。

void RotateR(Node* grandfather)//右单旋
{
	Node* parent = grandfather->_left;
	Node* subLR = parent->_right;

	parent->_right = grandfather;//p的右子节点指向g
	grandfather->_left = subLR;//g的左子节点指向p的右子节点

	if (subLR)
		subLR->_parent = grandfather;//subLR存在,则更新它的父指针指向g

	Node* ppNode = grandfather->_parent;
	parent->_parent = ppNode;//p的父指针指向g的父节点
	grandfather->_parent = parent;//g的父指针指向p

	if (grandfather == _root)//g节点是根节点,更新根节点
	{
		_root = parent;
		_root->_parent = nullptr;
	}
	else//p不是根节点,更新ppNode的指向
	{
		if (ppNode->_left == grandfather)//parent在它的左边
			ppNode->_left = parent;
		else if (ppNode->_right == grandfather)//parent在它的右边
			ppNode->_right = parent;
		else
			assert(false);//到这里,说明程序有问题
	}

	grandfather->_col = RED;//更新p和g的节点颜色
	parent->_col = BLACK;
}

2.2 p节点在g节点的右侧,c节点在p节点的右侧—右右->左单旋

u不存在时的情况很简单,就不再演示。

假设有如下经过情况一调整而来的红黑树:

初识C++之红黑树_第10张图片

可以看到,此时p在g的右侧,c在p的右侧。在这种情况下,就需要使用左单旋。让p的右子节点指向g,g的右子节点指向p的左子节点,然后更新g、p和p的左子节点的父指针,最后让g的原父节点指向p。

旋转完后,同样的,将g的颜色调整为红色,p的颜色调整为黑色

初识C++之红黑树_第11张图片

void RotateL(Node* grandfather)//左单旋
{
	Node* parent = grandfather->_right;
	Node* subRL = parent->_left;

	parent->_left = grandfather;//p的左子节点指向g
	grandfather->_right = subRL;//g的右子节点指向p的左子节点

	if (subRL)
		subRL->_parent = grandfather;//subRL存在,更新父指针

	Node* ppNode = grandfather->_parent;
	parent->_parent = ppNode;//p的父指针指向g的原父节点
	grandfather->_parent = parent;//g的父指针指向p

	if (grandfather == _root)//grandfather是根节点,更新根节点
	{
		_root = parent;
		_root->_parent = nullptr;
	}
	else//g不是根节点
	{
		if (ppNode->_left == grandfather)//p在左
			ppNode->_left = parent;
		else if (ppNode->_right == grandfather)//p在右
			ppNode->_right = parent;
		else//走到这里,说明程序有问题
			assert(false);
	}

	grandfather->_col = RED;
	parent->_col = BLACK;//更新g和p的节点颜色
}

 2.3 单旋总结

由此,可以总结出红黑树的单旋情况。

当p在g的左侧,c在p的左侧——左左时,采用右单旋

当p在g的右侧,c在p的右侧——右右时,采用左单旋

当旋转完成后,将g的颜色改为红色,p的颜色改为黑色

3. u不存在/存在且为黑->双旋

和AVL树一样,红黑树也会存在需要双旋的情况。但是红黑树的旋转都是建立在u不存在/存在且为黑的条件之上的。

3.1 p节点在g节点的左侧,c节点在p节点的右侧——左右->左右双旋

如下图一个调整而来的红黑树:

初识C++之红黑树_第12张图片

经过向上调整后,g被视为c节点。此时,p节点在g节点的左侧,c节点在p节点的右侧。在这种情况下,单次旋转就不再生效,和AVL树一样,需要进行双旋。

首先是以p为基点进行左单旋。让c的左子节点指向p,p的右子节点指向c的左子节点,更新p、c和c的左子节点的父指针,再让p的原父节点g指向c。最后将c和p的颜色都调整为红。

初识C++之红黑树_第13张图片

第二步就是以g为基点进行右单旋。让c的右子节点指向g,g的左子节点指向c的右子节点,更新c、g和c的右子节点的父指针,最后让g的原父节点指向c。

旋转完成后,将g的颜色调整为红色,c的颜色调整为黑色

初识C++之红黑树_第14张图片

void RotateLR(Node* parent)//左右双旋
{
	Node* grandfather = parent->_parent;
	Node* cur = parent->_right;

	RotateL(parent);//以p为基点左单旋
	RotateR(grandfather);//以g为基点右单旋

	cur->_col = BLACK;
	grandfather->_col = RED;//更新c、g节点颜色
}

 3.2 p节点在g节点的右侧,c节点在p节点的左侧——右左->右左双旋

如下图一个经过情况一调整而来的红黑树:

初识C++之红黑树_第15张图片

可以看到,经过情况一调整后的该树,它的p节点在g节点的右侧,c节点在p节点的左侧。此时,就需要进行两次旋转。

首先以p为基点进行右单旋。让c的右子节点指向p,p的左子节点指向c的右子节点,分别更新c、p和c的右子节点的父指针,最后让p的原父节点g指向c。

旋转完后,将p和c的颜色都调整为红色:

初识C++之红黑树_第16张图片

第二步就是以g为基点进行左单旋。让c的左子节点指向g,g的右子节点指向c的左子节点,更新g、c和c的左子节点的父指针,最后让g的原父节点指向c。

旋转完后,将g的颜色调整为红色,c的颜色新调整为黑色

初识C++之红黑树_第17张图片

void RotateRL(Node* parent)//右左双旋
{
	Node* grandfather = parent->_parent;
	Node* cur = parent->_left;

	RotateR(parent);//以p为基点右单旋
	RotateL(grandfather);//以g为基点左单旋

	cur->_col = BLACK;
	grandfather->_col = RED;//更新c、g节点颜色
}

3.3 双旋总结

通过上面的归类,就可以整理出红黑树需要进行双旋调整的情况。

(1)当p为g的左侧,c为p的右侧(左右)时,需要进行左右双旋

(2)当p为g的右侧,c为p的左侧(右左)时,需要进行右左双旋

当完成两次旋转后,g的颜色调整为红色,c的颜色调整为黑色

5.红黑树调整总结

通过上面的阐述,红黑树如何调整就很清楚了。红黑树要调整,首先会经过“uncle存在且为红”的情况,在经过该情况调整后,才会出现“uncle不存在/存在且为黑”的第二种调整情况。

而“uncle不存在/存在且为黑”的情况有根据p和c的位置出现4中不同调整方案。

(1)当p在g的左侧,c在p的左侧——左左时,采用右单旋

(2)当p在g的右侧,c在p的右侧——右右时,采用左单旋

在只需进行单旋的情况下, 在旋转完后要将g的颜色调整为红,p的颜色调整为黑

(3)当p为g的左侧,c为p的右侧(左右)时,需要进行左右双旋

(4)当p为g的右侧,c为p的左侧(右左)时,需要进行右左双旋

在需要进行双旋的情况下,在两次旋转完后,需要将g的颜色调整为红色,c的颜色调整为黑色

如果仔细观察双旋情况就会发现,其实双旋改修改的节点的位置和单旋的是一样的,双旋中的c节点其实就是单旋中的p节点,只是图中为了显示两次旋转后的差异,没有修改命名。

如果大家已经学习过了AVL树,就会发现红黑树的调整方法和调整情况绝大部分都是一样的,都是左左->右单旋;右右->左单旋;左右->左右双旋;右左->右左双旋。区别一个在于红黑树在旋转前,会经过一次“uncle存在且为红”的调整;另一个就是AVL树需要更新平衡因子,而红黑树没有平衡因子,而是更新节点颜色。

6.红黑树数据插入模拟实现

有了上面红黑树面对不同情况的旋转调整,就可以写出红黑树的插入了:

#pragma once
#include
#include
#include

using namespace std;

namespace MyRBTree
{
	enum Colour//用枚举定义颜色
	{
		RED,
		BLACK
	};

	template 
	struct RBTreeNode
	{
		pair _kv;//kv模型
		RBTreeNode* _left;//左子结点
		RBTreeNode* _right;//右子节点
		RBTreeNode* _parent;//父节点
		Colour _col;//节点颜色

		RBTreeNode(const pair& kv)//构造函数
			: _kv(kv)
			, _left(nullptr)
			, _right(nullptr)
			, _parent(nullptr)
			, _col(RED)//节点颜色默认给红色
		{}
	};

	template
	class RBTree
	{
		typedef RBTreeNode Node;
	public:
		RBTree()
		{}

		bool insert(const pair& kv)//数据插入
		{
			//如果头节点是空,则是第一次插入
			if (_root == nullptr)
			{
				_root = new Node(kv);
				_root->_col = BLACK;//头节点的颜色为空

				return true;
			}

			Node* parent = nullptr;
			Node* cur = _root;

			while (cur)//判断节点位置
			{
				if (kv.first < cur->_kv.first)//传进来的键值小于节点键值,向左走
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (kv.first > cur->_kv.first)//传进来的键值大于节点键值,向右走
				{
					parent = cur;
					cur = cur->_right;
				}
				else//走到这里,说明传进来的键值与节点键值相等,返回false
				{
					return false;
				}
			}

			cur = new Node(kv);//走到这里,说明找到插入位置,节点颜色默认设置为RED
			cur->_col = RED;

			if (kv.first < parent->_kv.first)//键值小于父节点键值,插入在左边
			{
				parent->_left = cur;
				cur->_parent = parent;
			}
			else if (kv.first > parent->_kv.first)//键值大于父节点键值, 插入在右边
			{
				parent->_right = cur;
				cur->_parent = parent;
			}
			else//走到这里,说明程序出现问题
				assert(false);

			//调整红黑树
			while (parent && parent->_col == RED)//如果parent的颜色为黑色,就无需调整
			{
				Node* grandfather = parent->_parent;//祖父节点
				if (grandfather->_left == parent)
				{
					Node* uncle = grandfather->_right;//父节点在左,叔叔节点在右
					
					if (uncle && uncle->_col == RED)//情况一:uncle存在且为红,调整p、u、g三个节点的颜色和修改指针
					{
						parent->_col = uncle->_col = BLACK;
						grandfather->_col = RED;

						cur = grandfather;
						parent = cur->_parent;//更新c、p两个个节点,g和u节点等循环上去重新创建				
					}
					else if (uncle == nullptr || uncle->_col == BLACK)//情况二:uncle不存在/存在且为黑。
					{												  //此处分为左左和左右两种情况
						if (cur == parent->_left)//左左,右单旋
						{
							RotateR(grandfather);
						}
						else if (cur == parent->_right)//左右,左右双旋
						{
							RotateLR(parent);
						}

						break;
					}
				}
				else if (grandfather->_right == parent)
				{
					Node* uncle = grandfather->_left;//父节点在右,叔叔节点在左
					
					if (uncle && uncle->_col == RED)//情况一:uncle节点存在且为红
					{
						parent->_col = uncle->_col = BLACK;
						grandfather->_col = RED;

						cur = grandfather;
						parent = cur->_parent;//更新c、p两个节点,g和u等循环上去重新创建					
					}
					else if (uncle == nullptr || uncle->_col == BLACK)//情况二:uncle不存在/存在且为黑
					{
						if (cur == parent->_right)//右右,左单旋
						{
							RotateL(grandfather);
						}
						else if (cur == parent->_left)//右左,右左双旋
						{
							RotateRL(parent);
						}

						break;
					}
				}
				else
				{
					assert(false);//到这里,说明程序有问题
				}
			}
			_root->_col = BLACK;//默认将根节点设置为黑

			return true;
		}

		void RotateR(Node* grandfather)//右单旋
		{
			Node* parent = grandfather->_left;
			Node* subLR = parent->_right;

			parent->_right = grandfather;//p的右子节点指向g
			grandfather->_left = subLR;//g的左子节点指向p的右子节点

			if (subLR)
				subLR->_parent = grandfather;//subLR存在,则更新它的父指针指向g

			Node* ppNode = grandfather->_parent;
			parent->_parent = ppNode;//p的父指针指向g的父节点
			grandfather->_parent = parent;//g的父指针指向p

			if (grandfather == _root)//g节点是根节点,更新根节点
			{
				_root = parent;
				_root->_parent = nullptr;
			}
			else//p不是根节点,更新ppNode的指向
			{
				if (ppNode->_left == grandfather)//parent在它的左边
					ppNode->_left = parent;
				else if (ppNode->_right == grandfather)//parent在它的右边
					ppNode->_right = parent;
				else
					assert(false);//到这里,说明程序有问题
			}

			grandfather->_col = RED;//更新p和g的节点颜色
			parent->_col = BLACK;
		}

		void RotateL(Node* grandfather)//左单旋
		{
			Node* parent = grandfather->_right;
			Node* subRL = parent->_left;

			parent->_left = grandfather;//p的左子节点指向g
			grandfather->_right = subRL;//g的右子节点指向p的左子节点

			if (subRL)
				subRL->_parent = grandfather;//subRL存在,更新父指针

			Node* ppNode = grandfather->_parent;
			parent->_parent = ppNode;//p的父指针指向g的原父节点
			grandfather->_parent = parent;//g的父指针指向p

			if (grandfather == _root)//grandfather是根节点,更新根节点
			{
				_root = parent;
				_root->_parent = nullptr;
			}
			else//g不是根节点
			{
				if (ppNode->_left == grandfather)//p在左
					ppNode->_left = parent;
				else if (ppNode->_right == grandfather)//p在右
					ppNode->_right = parent;
				else//走到这里,说明程序有问题
					assert(false);
			}

			grandfather->_col = RED;
			parent->_col = BLACK;//更新g和p的节点颜色
		}

		void RotateLR(Node* parent)//左右双旋
		{
			Node* grandfather = parent->_parent;
			Node* cur = parent->_right;

			RotateL(parent);//以p为基点左单旋
			RotateR(grandfather);//以g为基点右单旋

			cur->_col = BLACK;
			grandfather->_col = RED;//更新c、g节点颜色
		}

		void RotateRL(Node* parent)//右左双旋
		{
			Node* grandfather = parent->_parent;
			Node* cur = parent->_left;

			RotateR(parent);//以p为基点右单旋
			RotateL(grandfather);//以g为基点左单旋

			cur->_col = BLACK;
			grandfather->_col = RED;//更新c、g节点颜色
		}

	private:
		Node* _root;
	};
}

对于红黑树,对当前的我们来说只需要掌握如何插入构建一棵红黑树即可,对于红黑树的删除、替换等操作暂时不用过多了解。

你可能感兴趣的:(C++,算法,数据结构,c++,红黑树)