【C++】红黑树

本文详细介绍了红黑树的基本概念、节点定义、插入操作以及旋转调整策略,包括cur节点为红、parent节点为红、grandfather节点为黑、uncle存在且为红的情况,以及cur节点在左子树和右子树的不同调整。红黑树通过特定的平衡规则确保查找效率接近O(logN),在性能上与AVL树相比,虽然略逊一筹,但更适用于频繁插入操作的场景。
摘要由CSDN通过智能技术生成

一:红黑树

       1.1:红黑树的概念

       1.2:红黑树节点的定义

二:红黑树的插入

三:红黑树的旋转调整

                3.1:cur节点为红,parent节点为红,grandfather节点为黑,uncle存在且为红

                3.2:cur节点在左子树,parent节点为红,grandfather节点为黑,uncle不存在或者存在且为黑

                3.3:cur节点在右子树,parent节点为红,grandfather节点为黑,uncle不存在或者存在且为黑

四:红黑树检验与性能比较

五:总结

///

///

一:红黑树:

1.1:红黑树的概念

红黑树,是一种二叉搜索树,

但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。

通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的

红黑树和AVL树一样,都是在二叉搜索树的基础上通过旋转等手段来保持平衡,提高效率,但是两者在旋转的力度上却有着明显的差别。AVL树追求极致的平衡,需要频繁的进行旋转调整,更差的是在删除时,甚至一路要旋转调整到根节点去,所以,当要求结构较为稳定的场所,AVL树就显得不是那么合适了;
而红黑树和AVL树相比就没有那么“偏执”,虽然也需要旋转调整,但是区别于AVL树的绝对平衡,在AVL树一定旋转的情况下,同样的红黑树却有一部分不需要旋转,只需要改变其节点的颜色就可以维护红黑树所要求的平衡,那么红黑树是如何通过限制条件来保持平衡的呢?

红黑树具体有五个性质要求:

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

 

第一条性质和第二条性质以及第五条性质非常容易理解,很直接的告诉了我们,下面具体详细讲解第三条性质和第四条性质;

如果一个节点是红色的,则它的两个孩子结点是黑色的

 如图所示,8节点如果为红色,那么它的两个孩子节点的颜色必须同时为黑色,像上图8节点的子节点是红色的是不允许的;

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

 通过上图,我们可以发现,每一条路径的黑色节点都是2(不包括空节点);红黑树最短路径要求节点全黑,而最长路径则是一黑一红,黑红相间

所以,通过性质1-5合起来约束了该树的平衡性能--即该树上的最长路径不可能会大于2倍最短路径。为什么?

因为性质一该树上的节点非红即黑,由于性质三该树上不允许存在两个连续的红节点,那么对于从一个节点到其叶子节点的一条最长的路径一定是红黑交错的,那么最短路径一定是纯黑色的节点;

而又因性质四,从任一节点到其叶子节点的所有路径上都包含相同数目的黑节点,这么来说最长路径上的黑节点的数目和最短路径上的黑节点的数目相等!而又性质而根结点为黑、性质三叶子节点是黑,那么可知:

最长路径<=2*最短路径。一颗二叉树的平衡性能越好,那么它的效率越高!那么,我们可以将红黑树的性能大致看为【O(logN),2*O(logN)】;显然红黑树的平衡性能比AVL的略差些,但是经过大量试验证明,实际上红黑树的效率还是很不错了,仍能达到O(logN);

也就是说,如果有10亿个数据,最短路径包含的黑色节点个数大约是30个,而最长路径包含的黑色节点个数大概为60个左右。对于计算机CPU的运算速度而言,30个和60个几乎是一样的,所以它们的搜索效率也可以看成是一样的。

同时通过这五条性质也可以反推红黑树不成立的条件,比如,有连续的红色节点就不是红黑树,最长路径超过最短路径2倍的也不是红黑树等,这是我们后面检验是否为红黑树的重要手段!

///

1.2:红黑树节点的定义:

红黑树与AVL树一样,采取三叉链来实现主体结构,但是不同的是,AVL树通过平衡因子判断控制平衡,而红黑树通过节点颜色来判断控制平衡,所以红黑树的成员是三叉链+颜色;具体代码如下所示:

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)//节点的默认颜色给红色
		{

		}
	};

为什么节点默认颜色给红色呢?

因为节点颜色默认给红色,那么将会违反性质三中所说的不能出现连续的红节点,

如果节点颜色默认给黑色,那么将会违反性质四中所说的每一个路径上的黑色节点的个数相同;

假如违反性质三,节点默认给红色,那么有可能只需要调节树的一部分,甚至不需要调整就可以解决(主要通过变色,插入时会详细解释)

假如违反性质四,节点默认给黑色,那么肯定需要从插入的地方不断向上旋转调整,直至根节点;

两者一相比,违反性质三远比违反性质四的代价要小,所以节点默认应该给红色!

///

///

二:红黑树的插入:

红黑树的插入与AVL树的插入相同,都是按照比节点大的往右树插入,比节点小的往左树插入,插入的区别只在于后面的旋转调整,具体代码如下所示:

	bool Insert(const pair<K, V>& kv)
		{
			if (_root == nullptr)//如果整颗树为空
			{
				_root = new Node(kv);
				_root->_col = BLACK;//那么根节点颜色给黑色,满足性质二
				return true;
			}
			Node* parent1 = nullptr;
			Node* cur = _root;
			while (cur != nullptr)
			{
				if (cur->_kv.first < kv.first)//比节点大,向右树插入
				{
					parent1 = cur;
					cur = cur->_right;
				}
				else if (cur->_kv.first > kv.first)//比节点小,向左树插入
				{
					parent1 = cur;
					cur = cur->_left;
				}
				else//和节点的值相等
				{
					//cout << "要插入的值已经存在" << endl;
					return false;
				}
			}

            //找到空,开始插入,链接;
			cur = new Node(kv);
			cur->_col = RED;//确保插入节点的颜色为红节点;
			if (parent1->_kv.first < kv.first)//判断新节点插入到父节点的左还是右
			{
				parent1->_right = cur;
			}
			else //if (parent1->_kv.first > kv.first)
			{
				parent1->_left = cur;
			}
			cur->_parent = parent1;//插入的新节点指向父节点;
        
            //判断,旋转调整。。。。。。。
}

///

//

三:红黑树的旋转调整:

  3.1:cur节点为红,parent节点为红,grandfather节点为黑,uncle存在且为红

cur节点为将要插入的节点;       parent节点为要插入节点的父节点;                grandfather节点为父节点的父节点;

uncle节点为grandfather节点子节点:

1.parent节点如果是grandfather节点的左节点,那么uncle就是grandfather节点的右节点,

2.parent节点如果是grandfather节点的右节点,那么uncle就是grandfather节点的左节点,

cur节点为红,parent节点为红,grandfather节点为黑,uncle存在且为红时,我们只需要调整变色即可,具体变色过程如下所示:

 这只是变色调整的大致过程,接下来给出两个示例,方便说明:

情况一:grandfather节点就是根节点:

 当grandfather节点为根节点时,则在完成调整变色时,需要将其头节点置黑,满足性质二;

情况二:grandfather节点不是根节点:

 

所以,无论是情况一,还是情况二,都只需要变色调整就可以了,不需要旋转,也就是与AVL的最大差别,只需要记住变色的规律即可;

那么,在 cur节点为红,parent节点为红,grandfather节点为黑,uncle存在且为红  这种树形下 变色的规律:

1:parent节点与uncle节点变黑,grandfather节点变红,

2:如果grandfather节点为根节点,那么现在调整的是整颗树,则将其变成黑色,

      如果grandfather节点不为根节点,那么现在调整的是树的某一颗子树,重新计数cur,parent,uncle,grandfather节点的位置,接着调整,当       grandfather为根节点时,则将其变成黑色

具体代码如下所示:

              //插入新节点后开始调整
             while (parent1 != nullptr && parent1->_col == RED)//如果插入节点的父节点存在且为红,那么就出现了连续的红节点,需要调整变色;
 			{
				Node* grandfather = parent1->_parent;//记录一下父节点的父节点;
				if (grandfather->_left == parent1)//如果插入的节点在左子树;
				{
					Node* uncle = grandfather->_right;//那么uncle节点就在右子树;
					if (uncle != nullptr && uncle->_col == RED)//如果uncle节点存在且为红;
					{
                        //父节点与uncle节点变黑,grandfather节点变红;
						parent1->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//接着向上调整
						cur = grandfather;
						parent1 = cur->_parent;
					}
					else//如果uncle节点不存在 或者  存在且为黑;
                    {
                        //旋转调整变色;
                    }
                else if (grandfather->_right==parent1)//如果插入的节点在右子树;
				{
					Node* uncle = grandfather->_left;那么uncle节点就在左子树;
					if (uncle != nullptr && uncle->_col == RED)//如果uncle节点存在且为红;
					{

                        //父节点与uncle节点变黑,grandfather节点变红
						parent1->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//向上调整
						cur = grandfather;
						parent1 = cur->_parent;
					}
					else//如果uncle节点不存在 或者  存在且为黑;
					{
						 //旋转调整变色;
					}
				}

             }
            _root->_col = BLACK;//以防万一,为了符合性质二,将其头节点颜色变成黑
			return true;

///

3.2:cur节点在左子树,parent节点为红,grandfather节点为黑,uncle不存在或者存在且为黑

cur节点为将要插入的节点;       parent节点为要插入节点的父节点且parent节点在左子树;                grandfather节点为父节点的父节点;

uncle节点为grandfather节点子节点:uncle节点现在有两种可能,uncle节点不存在,  或者uncle节点存在且为黑;

cur节点插入在parent节点的左边和cur节点插入到parent节点是不一样的,如上图所示,  但是这只是变色旋转调整的大致过程,接下来给出两个示例,方便说明:

当uncle节点不存在时:

 

 当uncle节点存在且为黑时:

 所以在 cur节点在左子树,parent节点为红,grandfather节点为黑,uncle不存在或者存在且为黑 又有两种情况:

1:如果cur节点插入到parent节点的左边,那么树的形状类似于“/”形:

解决方法:那么只需要以grandfather节点右旋,然后再将parent节点变黑,grandfather节点变红;

2:如果cur节点插入到parent节点的右边,那么树的形状类似于“<”形:

解决方法:那么只需要先以parent节点左旋,再以grandfather节点右旋,然后再将cur节点变黑,parent节点变红,grandfather节点变红;

具体代码如下所示(这里的左旋右旋与AVL树的旋转大致是一模一样的):

//判断,旋转调整
			while (parent1 != nullptr && parent1->_col == RED)//插入新节点后开始调整
			{
				Node* grandfather = parent1->_parent;
				if (grandfather->_left == parent1)//如果parent节点在左树
				{
					Node* uncle = grandfather->_right;
					if (uncle != nullptr && uncle->_col == RED)
					{
						parent1->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//接着向上调整
						cur = grandfather;
						parent1 = cur->_parent;
					}
					else//如果uncle节点不存在或者为黑
					{
						if (parent1->_left == cur)//如果插入节点在父亲节点的左边
						{
                              //树的形状如下所示时:
							  //       g
							  //    p      u
							  // c
							RotateR(grandfather);//以grandfather节点右旋;
                            
                            //变色
							parent1->_col = BLACK;
							grandfather->_col = RED;
						}
						else//如果插入节点在父亲节点的右边
						{
                                //树的形状如下所示时
							    //      g
							    //  p       u
							    //      c
							RotateL(parent1);//先以parent节点左旋;
							RotateR(grandfather);//再以grandfather节点右旋;

                            //变色
							cur->_col = BLACK;
							parent1->_col = RED;
							grandfather->_col = RED;
						}
						break;
					}
				}
				else if (grandfather->_right==parent1)//如果parent节点在右树
				{
					Node* uncle = grandfather->_left;
					if (uncle != nullptr && uncle->_col == RED)
					{
						parent1->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//向上调整
						cur = grandfather;
						parent1 = cur->_parent;
					}
					else//如果uncle节点不存在或者为黑;
					{
						//旋转加变色;
					}
				}
			}
			_root->_col = BLACK;//以防万一,为了符合性质二,将其头节点颜色变成黑
			return true;
		}

///

3.3:cur节点在右子树,parent节点为红,grandfather节点为黑,uncle不存在或者存在且为黑

cur节点为将要插入的节点;       parent节点为要插入节点的父节点且parent节点在右子树;                grandfather节点为父节点的父节点;

uncle节点为grandfather节点子节点:uncle节点现在有两种可能,uncle节点不存在,  或者uncle节点存在且为黑;

 cur节点插入在parent节点的左边和cur节点插入到parent节点是不一样的,如上图所示,  但是这只是变色旋转调整的大致过程,接下来给出两个示例,方便说明:

当uncle节点不存在时:

 

当uncle节点存在且为黑时:

 

所以在 cur节点在右子树,parent节点为红,grandfather节点为黑,uncle不存在或者存在且为黑 又有两种情况:

1:如果cur节点插入到parent节点的右边,那么树的形状类似于“\”形:

解决方法:那么只需要以grandfather节点左旋,然后再将parent节点变黑,grandfather节点变红;

2:如果cur节点插入到parent节点的左边,那么树的形状类似于“>”形:

解决方法:那么只需要先以parent节点右旋,再以grandfather节点左旋,然后再将cur节点变黑,parent节点变红,grandfather节点变红;

具体代码如下所示(这里的左旋右旋与AVL树的旋转大致是一模一样的):

                    Node* uncle = grandfather->_left;
					if (uncle != nullptr && uncle->_col == RED)
					{
						parent1->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//向上调整
						cur = grandfather;
						parent1 = cur->_parent;
					}
					else
					{
						if (parent1->_right == cur)
						{
							   //     g
							   // u      p
							   //           c
							RotateL(grandfather);
							parent1->_col = BLACK;
							grandfather->_col = RED;
						}
						else
						{
							   //      g
							   //  u       p
							   //      c
							RotateR(parent1);
							RotateL(grandfather);
							cur->_col = BLACK;
							parent1->_col = RED;
							grandfather->_col = RED;
						}
					}

插入总结:

 

插入的全部实现如下所示:

bool Insert(const pair<K, V>& kv)
		{
			if (_root == nullptr)//如果整颗树为空
			{
				_root = new Node(kv);
				_root->_col = BLACK;//那么根节点颜色给黑色,满足性质二
				return true;
			}
			Node* parent1 = nullptr;
			Node* cur = _root;
			while (cur != nullptr)
			{
				if (cur->_kv.first < kv.first)//比节点大,向右树插入
				{
					parent1 = cur;
					cur = cur->_right;
				}
				else if (cur->_kv.first > kv.first)//比节点小,向左树插入
				{
					parent1 = cur;
					cur = cur->_left;
				}
				else//和节点的值相等
				{
					//cout << "要插入的值已经存在" << endl;
					return false;
				}
			}

			//找到空,开始插入,链接;
			cur = new Node(kv);
			cur->_col = RED;//确保插入节点的颜色为红节点;
			if (parent1->_kv.first < kv.first)//判断新节点插入到父节点的左还是右
			{
				parent1->_right = cur;
			}
			else //if (parent1->_kv.first > kv.first)
			{
				parent1->_left = cur;
			}
			cur->_parent = parent1;//插入的新节点指向父节点;
//
			//判断,旋转调整
			while (parent1 != nullptr && parent1->_col == RED)//插入新节点后开始调整
			{
				Node* grandfather = parent1->_parent;
				if (grandfather->_left == parent1)
				{
					Node* uncle = grandfather->_right;
					if (uncle != nullptr && uncle->_col == RED)
					{
						parent1->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//接着向上调整
						cur = grandfather;
						parent1 = cur->_parent;
					}
					else
					{
						if (parent1->_left == cur)
						{
							  //       g
							  //    p      u
							  // c
							RotateR(grandfather);
							parent1->_col = BLACK;
							grandfather->_col = RED;
						}
						else
						{
							    //      g
							    //  p       u
							    //      c
							RotateL(parent1);
							RotateR(grandfather);
							cur->_col = BLACK;
							parent1->_col = RED;
							grandfather->_col = RED;
						}
						break;
					}
				}
				else if (grandfather->_right==parent1)
				{
					Node* uncle = grandfather->_left;
					if (uncle != nullptr && uncle->_col == RED)
					{
						parent1->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//向上调整
						cur = grandfather;
						parent1 = cur->_parent;
					}
					else
					{
						if (parent1->_right == cur)
						{
							   //     g
							   // u      p
							   //           c
							RotateL(grandfather);
							parent1->_col = BLACK;
							grandfather->_col = RED;
						}
						else
						{
							   //      g
							   //  u       p
							   //      c
							RotateR(parent1);
							RotateL(grandfather);
							cur->_col = BLACK;
							parent1->_col = RED;
							grandfather->_col = RED;
						}
					}
				}
			}
			_root->_col = BLACK;
			return true;
		}

///

///

四:红黑树检验与性能比较

4.1:判断一颗树是否为红黑树:

和AVL树一样,也需要验证一下我们的红黑树写的是否正确。不能只看插入的结果,插入的结果不一定就能说明它是红黑树,所以我们只能通过写验证函数来判断是否为红黑树;

 前面我们说过,红黑树满足五条性质,那么我们也可以通过这五条性质来反推红黑树不成立的场景,

当根为空时,它是红黑树;

当根为红色时,违反了性质二,它一定不是红黑树;

当红黑树任意两条路径上的黑色节点不相等时,也一定不是红黑树;

当有连续红色节点时,也一定不是红黑树;

所以根据这些条件我们就可以多方面验证任意一棵树是否为红黑树;

写一个验证红黑树的函数:

        bool _IsBalance(Node* root, int black_num, int benchmark)
		{
			if (root == nullptr)
			{
				if (black_num != benchmark)
				{
					cout << "黑色节点不相等" << endl;
					return false;
				}
				return true;
			}
			if (root->_col == BLACK)
			{
				++black_num;
			}
			if (root->_col == RED && root->_parent != nullptr 
                                                        && root->_parent->_col == RED)
			{
				cout << "存在父子连续的红色节点" << endl;
				return false;
			}

			return _IsBalance(root->_left, black_num, benchmark)
                     && _IsBalance(root->_right, black_num, benchmark);
		}




        bool IsBalance()
		{
			int benchmark = 0;
			if (_root == nullptr)
			{
				return true;
			}

			if (_root != nullptr && _root->_col == RED)
			{
				cout << "根为红色" << endl;
				return false;
			}

			Node* cur = _root;
			while (cur != nullptr)
			{
				if (cur->_col == BLACK)
				{
					++benchmark;
				}
				cur = cur->_left;
			}

			return _IsBalance(_root, 0, benchmark);
		}

 

我们可以通过给出一些测试用例来检验一下我们的插入与检验函数是否正确:

给出具体用类测试一下:

 检验时,返回类型是bool,所以打印出为1,而通过中序遍历,我们可以将红黑树按有序升序打印出来,当然,这里就几个数,插入旋转调整的少,我们可以通过随机数种子多插入一些值,具体如下所示:

这里通过生成500000个随机数进行插入,也通过了检验,这证明这我们插入时的代码和检验的代码应该大体上没有什么问题; 

4.2:红黑树与AVL树的性能比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log_2 N),

红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,
所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多


至于红黑树的删除和AVL树的删除一样,效率低,而且基本上都要一路调整变色到根节点上,较为复杂,如果感兴趣的话,可以观看《算法导论》或者《STL源码剖析》,这两本书中有较为详细的解释;

五:总结:

红黑树是较为重要的数据结构,有着广泛的应用,不止在C++中,在java,linux也有许多应用,而红黑树不像AVL树那么的对平衡要求那么高,旋转那么频繁,所以在STL库中,set,map等容器的底层都是红黑树,了解红黑树,也为我们认识与运用set,map等关联容器打下坚实的基础;

抱歉,我是一名语言模型AI,无法为您提供演示实时代码功能。但是,我可以向您解释红黑树的基本原理和操作。红黑树是一种自平衡二叉搜索树,它的每个节点都有一个颜色属性,红色或黑色,具有以下特性: 1. 根节点为黑色。 2. 所有叶子节点(NIL节点)为黑色。 3. 如果一个节点为红色,则它的两个子节点必须都为黑色。 4. 从任意一个节点到其每个叶子节点的所有路径都包含相同数量的黑色节点。 红黑树的基本操作包括插入、删除和查找。插入和删除操作会破坏红黑树的平衡性,需要通过旋转和重新着色来修复平衡。查找操作与二叉搜索树相同,具有O(log n)的时间复杂度。 以下是红黑树的插入操作的伪代码: 1. 新节点插入到红黑树中,颜色为红色。 2. 如果新节点的父节点为黑色,则不需要做任何操作,仍然平衡。 3. 如果新节点的父节点为红色,需要进行以下操作: (1) 如果新节点的叔节点(父节点的兄弟节点)为红色,则将父节点和叔节点涂黑,祖父节点涂红,然后将当前节点指向祖父节点,重复步骤2。 (2) 如果新节点的叔节点为黑色,并且新节点是父节点的右子节点(父节点为祖父节点的左子节点),则将父节点左旋转,将当前节点指向父节点,重复步骤4。 (3) 如果新节点的叔节点为黑色,并且新节点是父节点的左子节点(父节点为祖父节点的右子节点),则将父节点右旋转,将当前节点指向父节点,重复步骤4。 4. 将父节点涂黑,祖父节点涂红,然后进行以下操作: (1) 如果当前节点是父节点的左子节点,将祖父节点右旋转。 (2) 如果当前节点是父节点的右子节点,将祖父节点左旋转。 以上是红黑树的基本操作,希望能够帮助您理解红黑树的原理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值