红黑树介绍与实现

Posted 可乐不解渴

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了红黑树介绍与实现相关的知识,希望对你有一定的参考价值。

红黑树介绍与实现


尽力做好一件事,实乃人生之首务。

红黑树的概念

红黑树是指每个节点都带有颜色属性的二叉搜索树,其颜色不是红色就是黑色,并且要在二叉搜索树的基础上要满足一些特殊的性质,因此被我们称之为红黑树。

红黑树的性质

一棵红黑树是满足下面红黑性质的二叉搜索树:

  1. 每个结点不是红色就是黑色。
  2. 根结点是黑色的。
  3. 如果一个结点是红色的,则它的两个子结点都是黑色的 (即不能出现连续的红结点)。
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。
  5. 每个叶节点(NIL)是黑色的 (此处的叶子结点指的是空结点)。

    推论:
    最短路径:全部都是由黑色结点构成。
    最长路径:一黑一红,红色结点的数量和黑色结点的数量相同。
    注意:正常红黑树中,不一定有全黑的最短路径和一黑一红的最长路径。

红黑树结点的定义

这里我们使用枚举类型来定义红黑树的颜色,当然也可以利用如bool类型来表示都是可以的。其中这里我们将结点定义成K-V模型,这是为了后面的博客中我们要用这同一颗红黑树来模拟实现map与set容器。

代码如下:

//节点的颜色
enum class Color

	RED,
	BLACK
;

//K-V模型
template<class K,class V>
struct RBTreeNode	//三叉链

	RBTreeNode<K,V>* m_left;	//左节点
	RBTreeNode<K,V>* m_right;   //右节点
	RBTreeNode<K,V>* m_parent;  //节点的父结点(红黑树需要 旋转 ,为了实现简单给出的该字段)

	pair<K, V>m_kv; //节点数据
	Color m_col;	//节点颜色


	RBTreeNode(const pair<K, V>& kv)
		: m_left(nullptr), m_right(nullptr), m_parent(nullptr)
		, m_kv(kv),m_col(Color::RED)
	
;

红黑树的插入操作

在我们要进行插入操作时,我们要想一个问题我们是插入黑色结点好,还是插入红色结点好呢?下面我们进行分析:

  1. 新插入的结点是红色,可能会破坏性质3,这导致有连续的红色结点,但也只影响这一条路径,影响不大。
  2. 新插入的结点是黑色,一定会破坏性质4,这导致每条路径的黑色结点数目不相同,破坏性强!!!

综上所述,我们选择新插入结点时插入红色结点。

红黑树是在二叉搜索树的基础上加上一些特殊的平衡限制性质,因此红黑树的插入与二叉搜索树插入类似,具体红黑树插入可分为两步:

  1. 先按照二叉搜索树的规则先找到插入位置,并插入。
  2. 再检测这个新插入的结点插入后,红黑树的性质是否遭到破坏。

由于新结点插入的颜色默认是红色,因此如果双亲parent为黑色,则不需要调整,直接插入完成,退出插入操作。
但如果双亲parent为红色时,这样就违反了性质3不能存在连续的红色结点的情况,因此需要对红黑树来进行调整,具体情况如下所示

  • 情况一:cur为红(新增结点),parent为红,grandfather为黑,uncle存在且为红。
    处理方案为:parent和uncle变为黑色,grandfather变为红,但调整还没有结束,因为此时祖父结点变成了红色,我们要再判断祖父的父结点是否为红色,若其父结点也是红色,那么又需要根据其叔叔的不同,进而进行不同的调整操作。然后继续向上处理,直到parent存在且为黑的时候停止,或者parent不存在停止,如果grandfather是根,那么将根变黑。

  • 情况二:cur为红(新增结点),parent为红,且cur与parent为直线的时候,uncle不存在或者存在且为黑。
    处理方案为:旋转加变色处理,grandfather变红,parent变黑。
    并且uncle存在,一定是黑色,那么cur结点原来的颜色一定也是黑色的,现在看到是红色的原因是因为cur的子树再颜色调整的过程中将cur结点的颜色由黑色改变成红色,即从情况1向上处理后碰到情况2的情况,具体如下图中的例2所示:
  • 情况三:cur为红(新增结点),parent为红,且cur与parent为折线的时候,uncle不存在或者存在且为黑。
    处理方案为:双旋加变色处理,cur变黑,grandfather变红。

    代码如下:
	//传进来的是不平衡的结点的指针
	void RotateL(Node* parent)	//左旋
	
		Node* subR = parent->m_right;	//出问题的右结点
		Node* subRL = subR->m_left;		//出问题的右结点的左结点

		parent->m_right = subRL;		//首先先将我们的parent的右指向subRL
		if (subRL != nullptr)			//再将subRL的父亲指向parent结点,但这样要判断一下是否是空指针,如果subRL是空指针的话,
										//那么解引用它会出现问题
		
			subRL->m_parent = parent;
		

		Node* curParent = parent->m_parent;	//拿一个结点存储parent的父亲
		parent->m_parent = subR;			//再使得parent的父亲指针指向它原先的右结点(subR)

		subR->m_left = parent;				//在让subR的左指向parent
		if (parent == m_root)				//在这里得判断一下它是否为根,如果parent为根的话,那么我们的根结点指针也得改变
											//并且将subR的父亲指针置为空,此时subR此时为根结点
		
			m_root = subR;
			subR->m_parent = nullptr;
		
		else
											//如果不为头节点,那么我们只需要将subR的父亲指针指向parent之前的父亲结点
			if (curParent->m_left == parent)
			
				curParent->m_left = subR;
			
			else
			
				curParent->m_right = subR;
			
			subR->m_parent = curParent;
		
	



	void RotateR(Node* parent)	//右旋
	
		Node* subL = parent->m_left;
		Node* subLR = subL->m_right;

		parent->m_left = subLR;
		if (subLR != nullptr)
		
			subLR->m_parent = parent;
		

		Node* curParent = parent->m_parent;
		parent->m_parent = subL;

		subL->m_right = parent;
		if (parent == m_root)
		
			m_root = subL;
			subL->m_parent = nullptr;
		
		else
		
			if (curParent->m_left == parent)
			
				curParent->m_left = subL;
			
			else
			
				curParent->m_right = subL;
			
			subL->m_parent = curParent;
		

	
	//插入看叔叔
	std::pair<Node*, bool> insert(const std::pair<K, V>& kv)
	
		if (m_root == nullptr) //如果红黑树为空树,直接插入结点并将该结点作为根结点即可
		
			m_root = new Node(kv);
			m_root->m_col = Color::BLACK;
			return std::make_pair(m_root, true);
		
		//按照二叉搜索树的插入方法,找到要插入的位置
		Node* parent = nullptr;
		Node* cur = m_root;
		while (cur != nullptr)
		
			if (cur->m_kv.first > kv.first)
			
				parent = cur;
				cur = cur->m_left;
			
			else if (cur->m_kv.second < kv.first)
			
				parent = cur;
				cur = cur->m_right;
			
			else
			
				return std::make_pair(cur, false);
			
		
		//判断新增结点这个位置是在该位置parent的左边还是右边
		//并将其链接起来
		Node* newNode = new Node(kv);
		if (parent->m_kv.first > kv.first)
		
			parent->m_left = newNode;
			newNode->m_parent = parent;
		
		else
		
			parent->m_right = newNode;
			newNode->m_parent = parent;
		
		cur = newNode;
		
		//3、调颜色,如果插入的结点的父亲是红色得到我们就需要进行调整
		while (parent != nullptr && parent->m_col == Color::RED)
		
			Node* grandFather = parent->m_parent;
			if (parent == grandFather->m_left) //情况一:叔叔存在且为红
			
				Node* uncle = grandFather->m_right;
				if (uncle != nullptr && uncle->m_col == Color::RED)
				
					uncle->m_col = parent->m_col = Color::BLACK;
					grandFather->m_col = Color::RED;
					cur = grandFather;
					parent = cur->m_parent;
				
				else
				
					if (cur == parent->m_left) //情况二:叔叔存在且为黑直线或者不存
					
						RotateR(grandFather);
						grandFather->m_col = Color::RED;
						parent->m_col = Color::BLACK;
					
					else //情况三:叔叔存在且为黑折线或者不存
					
						RotateL(parent);
						RotateR(grandFather);
						cur->m_col = Color::BLACK;
						grandFather->m_col = Color::RED;
					
					break;
				

			
			else
			
				Node* uncle = grandFather->m_left;
				if (uncle != nullptr && uncle->m_col == Color::RED)
				
					uncle->m_col = parent->m_col = Color::BLACK;
					grandFather->m_col = Color::RED;
					cur = grandFather;
					parent = cur->m_parent;
				
				else
				
					if (cur == parent->m_left)
					
						RotateR(parent);
						RotateL(grandFather);
						grandFather->m_col = Color::RED;
						cur->m_col = Color::BLACK;
					
					else
					
						RotateL(grandFather);
						grandFather->m_col = Color::RED;
						parent->m_col = Color::BLACK;
					
					break;
				
			
		
		m_root->m_col = Color::BLACK; //始终让根结点为黑色
		return std::make_pair(newNode, true);
	

在插入中的旋转方式,在这里就不在叙述了,看不懂的话可以看我的AVL树的博客在哪里有具体的讲解。

红黑树的验证

由于我们直到红黑树是一种满足某种特殊性质的二叉搜索树,因此我们可以先获取到二叉树的中序序列,来判断该二叉树是否满足二叉搜索树的性质。

其中中序遍历的代码如下:

private:
	void _Inorder(Node*& root)
	
		if (root == nullptr)
		
			return;
		
		_Inorder(root->m_left);
		cout << root->m_kv.first << " : " << root->m_kv.second << endl;
		_Inorder(root->m_right);
	
public:
	void Inorder()
	
		_Inorder(m_root);
		cout << endl;
	

在这里我们利用两个函数来进行书写,这样做的好处是,外层用户不会直接去使用到根结点,它只需要看到结果即可。但中序遍历出来有序,只能保证它是一棵二叉搜索树,并不能保证它是满足红黑树树的性质。
故我们还要验证是否满足红黑树的五条性质,原理是先从根开始计算最左路黑色结点个数,那么只需要从根开始遍历只要每条路径到空位置,黑色结点个数相同即可直到是否满足每条路径都有相同数量的黑色结点。
代码如下:

public:
	//判断是否为红黑树
	bool IsRBTree()
	
		if (m_root == nullptr) //空树是红黑树
		
			return true;
		
		if (m_root->m_col == Color::RED)
		
			cout << "error:根结点为红色" << endl;
			return false;
		

		//找最左路径作为黑色结点数目的参考值
		Node* cur = m_root;
		int BlackCount = 0;
		while (cur)
		
			if (cur->m_col == Color::BLACK)
				BlackCount++;
			cur = cur->m_left;
		

		int count = 0;
		return _IsRBTree(m_root, count, BlackCount);
	
private:
	//判断是否为红黑树的子函数
	bool _IsRBTree(Node* root, int count, int BlackCount)
	
		if (root == nullptr) //该路径已经走完了
		
			if (count != BlackCount)
			
				cout << "error:黑色结点的数目不相等" << endl;
				return false;
			
			return true;
		

		if (root->m_col == Color::RED && root->m_parent->m_col == Color::RED)
		
			cout << "error:存在连续的红色结点" << endl;
			return false;
		
		if (root->m_col == Color::BLACK)
		
			count++;
		
		return _IsRBTree(root->m_left, count, BlackCount) && _IsRBTree(root->m_right, count, BlackCount);
	

故红黑树的验证分为以下两步:

  1. 通过中序遍历来判断是否是二叉搜索树,因为首先要满足是搜索树这是红黑树的前提。
  2. 在搜索树的前提下,再通过验证是否满足红黑树的性质来判断是否是红黑树。

红黑树的删除

红黑树的删除大致和AVL树的删除差不多,只是在AVL树删除是要调节平衡因子,而红黑树删除是要调节使其满足红黑树的五条性质。
与AVL树删除同样。

  • 首先都是先找到要删除的结点,并用替代法将结点删除。因此我们最终需要删除的都是左右子树至少有一个为空的结点。找到实际待删除结点后,先不直接删除该结点,我们先调整红黑树让其删除后任然满足红黑性质,所以我们找到实际待删除结点之后立即对红黑树进行调整。
  • 其次就是调整红黑树,使其删除该结点之后仍然满足红黑树的性质。最后就是删除,并把链接关系建立好即可。

我们先从最简单的出发,如果要删除的节点是 a,它只有一个子节点 b,那我们就依次进行下面的操作。
处理方式为:删除节点 b,并且把节点 b的数据替换到节点 a 的位置。并且这个节点 a 只能是黑色,节点 b 也只能是红色,其他情况均不符合红黑树的性质。这种调整结束后不需要进行二次调整,直接退出即可。

这里需要注意一下,红黑树的定义中“只包含红色节点和黑色节点”,经过初步调整之后,为了保证满足红黑树定义的最后一条要求,有些节点会被标记成两种颜色,“红 - 黑”或者“黑 - 黑”。如果一个节点被标记为了“黑 - 黑”,那在计算黑色节点个数的时候,要算成两个黑色节点。

在下面的讲解中,如果一个节点既可以是红色,也可以是黑色,在画图的时候,我会用一半红色一半黑色来表示。如果一个节点是“红 - 黑”或者“黑 -黑”,我会用左上角的一个小黑点来表示额外的黑色。 其中x、y、z、w、p、q等都是表示子树,可以只为NIL结点。

CASE 1:如果关注节点是 a,它的兄弟节点 c 是红色的,我们就依次进行下面的操作:

  1. 围绕关注节点 a 的父节点 b 左旋;
  2. 关注节点 a 的父节点 b 和祖父节点 c 交换颜色,关注节点不变,更新兄弟结点,此时兄弟结点就变为了d;

这样就从CASE 1 转变为 后面的CASE 2、3、4这三种情况继续进行调整。

CASE 2 : 如果关注节点是 a,它的兄弟节点 c 是黑色的,并且节点 c 的左右子节点 d、e 都是黑色的,我们就依次进行下面的操作:

  1. 将关注节点 a 的兄弟节点 c 的颜色变成红色;
  2. 从关注节点 a 中去掉一个黑色,这个时候节点 a 就是单纯的红色或者黑色;
  3. 给关注节点 a 的父节点 b 添加一个黑色,这个时候节点 b 就变成了“红 - 黑”或者“黑 - 黑”;
    3.1 此时结点b变为了“红 - 黑”,即b的颜色原来是单纯的红色时,我们将b的颜色变成黑色后结束调整;
    3.2 此时结点b变为了“黑 - 黑”,即b的颜色原来是单纯的黑色时,我们继续向上更新,最后我们将关注节点从 a 变成其父节点 b;

这样我们就继续从四种情况中选择符合的规则来进行调整。

CASE 3:如果关注节点是 a,它的兄弟节点 c 是黑色,c 的左孩子节点 d 是红色,c 的右孩子节点 e 是黑色,我们就依次进行下面的操作:

  1. 首先我们围绕关注节点 a 的兄弟节点 c 右旋;
  2. 其次是节点 c 和节点 d 交换颜色;关注节点不变,但要和CASE 1 一样要更新关注结点的兄弟结点;

从CASE 3 一定会跳转到 CASE 4,继续调整。

CASE 4:如果关注节点 a 的兄弟节点 c 是黑色的,并且 c 的右子节点是红色的,我们就依次进行下面的操作:

  1. 首先我们围绕关注节点 a 的父节点 b 进行左旋;
  2. 将关注节点 a 的兄弟节点 c 的颜色,跟关注节点 a 的父节点 b 设置成相同的颜色;然后将关注节点 a 的父节点 b 的颜色设置为黑色;从关注节点 a 中去掉一个黑色,节点 a 就变成了单纯的红色或者黑色;将关注节点 a 的叔叔节点 e 设置为黑色;

CASE 4 调整完后就结束调整。

CASE 1 、3 和4在各自执行常数次数的颜色改变和至多3次旋转后便终止。而CASE 2 是 while 循环可以重复执行的唯一情况,然后指针x沿树上升至多O(logn)次,且不执行任何旋转。

  • 最后就是进行结点的删除。删除该结点判断删除结点是否有孩子,将删除结点的孩子结点的m_parent链接到删除结点的父亲,然后再将删除结点的父亲的m_left或者m_right链接到孩子。

代码如下:

public:
	//删除函数 删除看兄弟
	bool erase(const K& key)
	
		//用于遍历二叉树
		Node* parent = nullptr;
		Node* cur = m_root;
		//用于标记实际的待删除结点及其父结点
		Node* delParentPos = nullptr;
		Node* delPos = nullptr;
		while (cur != nullptr)
		
			if (key < cur->m_kv.first) //所给key值小于当前结点的key值
			
				//往该结点的左子树走
				parent = cur;
				cur = cur->m_left;
			
			else if (key > cur->m_kv.first) //所给key值大于当前结点的key值
			
				//往该结点的右子树走
				parent = cur;
				cur = cur->m_right;
			
			else //找到了待删除结点
			
				if (cur->m_left == nullptr) //待删除结点的左子树为空
				
					if (cur == m_root) //待删除结点是根结点
					
						m_root = m_root->m_right; //让根结点的右子树作为新的根结点
						if (m_root)
						
							m_root->m_parent = nullptr;
							m_root->m_col = Color::BLACK; //根结点为黑色
						
						delete cur; //删除原根结点
						return true;
					
					else
					
						delParentPos = parent; //标记实际删除结点的父结点
						delPos = cur; //标记实际删除的结点
					
					break; //进行红黑树的调整以及结点的实际删除
				
				else if (cur->m_right == nullptr) //待删除结点的右子树为空
				
					if (cur == m_root) //待删除结点是根结点
					
						m_root = m_root->m_left; //让根结点的左子树作为新的根结点
						if (m_root)
						
							m_root->m_parent = nullptr;
							m_root->m_col = Color::BLACK; //根结点为黑色
						
						delete cur; //删除原根结点
						return true;
					
					else
					
						delParentPos = parent; //标记实际删除结点的父结点
						delPos = cur; //标记实际删除的结点
					
					break; //进行红黑树的调整以及结点的实际删除
				
				else //待删除结点的左右子树均不为空
				
					//替换法删除
					//寻找待删除结点右子树当中key值最小的结点作为实际删除结点
					Node* minParent = cur;
					Node* minRight = cur->m_right;
					while (minRight->m_left != nullptr)
					
						minParent = minRight;
						minRight = minRight->m_left;
					
					cur->m_kv.first = minRight->m_kv.first; //将待删除结点的key改为minRight的key
					cur->m_kv.second = minRight->m_kv.second; //将待删除结点的value改为minRight的value
					delParentPos = minParent; //标记实际删除结点的父结点
					delPos = minRight; //标记实际删除的结点
					break; //进行红黑树的调整以及结点的实际删除
				
			
		

		if (delPos == nullptr) //delPos没有被修改过,说明没有找到待删除结点
		
			return false;
		

		//记录待删除结点及其父结点(用于后续实际删除)
		Node* del = delPos;
		Node* delP = delParentPos;

		//调整红黑树 为了删除黑色结点后来恢复红黑树的性质
		if (delPos->m_col == Color::BLACK) //删除的是黑色结点
		
			if (delPos->m_left != nullptr) //待删除结点有一个红色的左孩子(不可能是黑色)
			
				delPos->m_left->m_col = Color::BLACK; //将这个红色的左孩子变黑即可
			
			else if (delPos->m_right != nullptr) //待删除结点有一个红色的右孩子(不可能是黑色)
			
				delPos->m_right->m_col = Color::BLACK; //将这个红色的右孩子变黑即可
			
			else //待删除结点的左右均为空
			
				while (delPos != m_root) 以上是关于红黑树介绍与实现的主要内容,如果未能解决你的问题,请参考以下文章

C++进阶数据结构_红黑树

红黑树与JAVA实现

AVL树红黑树以及B树介绍

红黑树详解——数据删除操作

红黑树

红黑树的特性与插入操作