【C++】一文搞懂红黑树:从理论到实战全覆盖

📚 博主的专栏

  🐧 Linux   |   🖥️ C++   |   📊 数据结构  | 💡C++ 算法 | 🅒 C 语言  | 🌐 计算机网络

上篇文章AVL树

下篇文章:红黑树模拟实现STL库 map_set

目录

红黑树的概念

红黑树的性质

如何做到最长路径 <= 最短路径*2

红黑树的插入

情况一: cur为红,p为红,g为黑,u存在且为红

解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。

a/b/c/d/e每条路径有x个黑色节点,红黑子树 x >=0。

一颗红黑树的黑色节点是由我们插入红节点后向上变色所增加的

抽象图:

情况二: cur为红,p为红,g为黑,u存在且为黑、u不存在

解决方式:

1.u不存在

2.u存在

情况二.5:cur为红,p为红,g为黑,u不存在/u存在且为黑

解决方式:

​编辑

insert代码编写

红黑树的验证


红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的。红黑树能比AVL树略高,旋转比AVL树更少一些。最长路径 <= 最短路径*2

AVL树:是一个严格平衡因子

红黑树:近似平衡++

红黑树的性质

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

如何做到最长路径 <= 最短路径*2

极端场景:

最长路径:一黑一红间隔

最短路径:全黑

控制红黑树

	enum Colour
	{
		BLACK,
		RED
	};
	template<class K, class V>
	struct RBTreeNode
	{
		//在K_V添加了parent
		RBTreeNode<K, V>* _left;
		RBTreeNode<K, V>* _right;
		RBTreeNode<K, V>* _parent;
		pair<K, V> _kv;

		Colour _col;	
	};

红黑树的插入

红黑树的插入操作

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

1. 按照二叉搜索的树规则插入新节点

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
    void insert();
	
private:
	Node* _root = nullptr;
	size_t _size = 0;
};

检测新节点插入后,红黑树的性质是否造到破坏

因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:

约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

新插入节点,必须插入红色(可能违反规则3),如果插入黑色,就会违反规则4每条路径都存在相同数量的黑色节点。

实际上就是对uncle的三种情况进行讨论,分析。u是否存在,存在是什么颜色?

情况一: cur为红,p为红,g为黑,u存在且为红

cur和p均为红,违反了性质三,此处能否将p直接改为黑?

解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。

g是否可以不变红,不行,g所在的这棵树,可能是整棵树的子树,不变红,子树路径的黑色节点数量都+1,破坏了规则4

红黑树的原则:永远不能破坏规则4(每条路径都存在相同数量的黑色节点)

如果g是根:就将g变黑

如果g不是根,就将g当成cur,继续向上调整,如果遇到父亲是黑色,那就结束了。

为了保证黑色节点的数量不变,以及补救插入红色节点导致的连续红色节点。

a/b/c/d/e每条路径有x个黑色节点,红黑子树 x >=0。

x == 1

这张图中,如果a\b、g的父亲是黑色的,就结束了,如果a\b、g的父亲是红色的就还需要继续处理

一颗红黑树的黑色节点是由我们插入红节点后向上变色所增加的

 也就是这样一个变化过程

计算子树x=1的红黑树一共会有多少种情况:

x == 1,子树有m/n/p/q四种情况,c/d/e是m/n/p/q四种情况中的任意一个,组合4^3 = 64。

新增节点插入位置是a或者b的孩子 2 + 2 = 4种情况,合计组合:64 * 4 = 256种

抽象图:

情况二: cur为红,p为红,g为黑,u存在且为黑、u不存在

解决方式:

p为g的左孩子,cur为p的左孩子,则进行右单旋转;

相反, p为g的右孩子,cur为p的右孩子,则进行左单旋转

p、g变色--p变黑,g变红

1.u不存在

cur是新插入的节点

需要将cur的父亲变黑,爷爷变红,右旋:

树的高度不变,和上级链接的节点也本就是黑色,因此,不需要再向上变色

2.u存在

如图: 之前一定是黑色的,保证不违反4

x == 1:

相反,这里还存在着一种情况,如果p连着的子树在左,u在右,最后就是左单旋

情况二.5:cur为红,p为红,g为黑,u不存在/u存在且为黑

解决方式:

p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;

相反, p为g的右孩子,cur为p的左孩子,则针对p做右单旋转

则转换成了类似情况2

insert代码编写

其中的旋转函数直接参考上篇文章AVL中的旋转函数

	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr) {  // 处理空树情况
			_root = new Node(kv);
			_root->_col = BLACK;  //初始节点给黑色
			return true;
		}
		Node* parent = nullptr;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < kv.first) //插入节点的值大于根节点的值,就往右走
			{
				parent = cur; //往右节点走之前,先存下parent
				cur = cur->_right;
			}
			else if (cur->_kv.first > kv.first)
			{
				parent = cur;

				cur = cur->_left;
			}
			else//默认定义(90%的情况下),搜索树不允许冗余,因此若值相等就不插入 
			{
				return false;
			}
		}
		cur = new Node(kv);
		cur->_col = RED;//新增节点给红色
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		//让每个父亲都指向父亲
		cur->_parent = parent;
		//向上找父亲,变颜色,红色的上面一定有父亲,因为红色不为根节点,由于下面的向上调整,这需要一开始就判断父亲是否存在
		while (parent && parent->_col == RED) //父亲颜色是黑色就结束,在循环结束的时候将根节点变为黑色
		{
 			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;
					//继续往上处理:
					cur = grandfather;
					parent = cur->_parent;
				}
				else //叔叔不存在,或者叔叔存在且为黑,都是使用右旋+变色
				{
					if (cur == parent->_left)
					{
						//	单旋
						//	   g		  p	
						//	p	  u		c	g
						//c					  u
						RotateR(grandfather);
						//将父亲变黑,爷爷变红
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//	双旋
						//	   g			g		     c
						//	p	  u  ——> c     u  ——> p	   g		
						//	   c		p				     u 
					//以parent为旋转点进行左单旋,再以grandfather进行右旋转,cur->black
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
				
			}
			else if (parent == grandfather->_right)
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					//把父亲和叔叔的颜色变黑
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;
					//继续往上处理:
					cur = grandfather;
					parent = cur->_parent;
				}
				else//叔叔不存在,或者叔叔存在且为黑,单旋则为左单旋,双旋则为右左双旋
				{	

					//..
					if (cur == parent->_right)
					{
						//	单旋
						//	   g			   p
						//	u	  p		——> g	  c
						//			c	 u	
						RotateL(grandfather);
						//将父亲变黑,爷爷变红
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else
					{
						//	   g			   g	        c
						//	u	  p		——> u	 c ——>   g	  p
						//	   c			       p   u
					//以parent为旋转点进行左单旋,再以grandfather进行右旋转,cur->black
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}	
		}
		//无论循环是怎么处理的,根始终都要为黑色
		_root->_col = BLACK;
		return true;
	}

红黑树的验证

红黑树的检测分为两步:

1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)

2. 检测其是否满足红黑树的性质

  • a、性质三遇到红色就看父亲是否是黑色
  • b、计算出每条路径黑色节点的数量(DFS深度遍历,前序遍历就是一种DFS)
    • 每个节点都记录一个黑色节点的数量,遇到黑色节点就+1记录后,走到空后再往回走传给根。
    • 添加一个形参。
    • 比较每条路径的个数
      1. 把每次的值都放到一个vector中,然后把vector里的值进行比对
      2. 先任意计算一条路径作为参考值,走最左或者最右路径
      3. 或者增加一个prev变量,记录上一条路径的值,用引用来比较

	bool IsBalance()
	{
		if (_root->_col == RED) //2.根必须是黑色
			return false;

		int refNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				++refNum;
			}
			cur = cur->_left;
		}
		return Check(_root, 0, refNum);//用0来表示黑色节点初始值
	}

private:

	bool Check(Node* root, int blackNum, const int refNum)
	{
		
		//根据红色找父亲,遍历整棵树
		if (root == nullptr)
		{
			cout << blackNum << endl;
			if (refNum != blackNum)
			{
				cout << "存在黑色节点数量不相等的路径" << endl;
				return false;
			}
			return true;
		}
			
		//3.不能存在连续的红色节点
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << root->_kv.first << "->存在连续的红色节点" << endl;
			return false;
		}
		//4.每条路径都存在相同数量的黑色节点————>深度遍历
		if(root->_col == BLACK)
		{
			blackNum++;
		}

		return Check(root->_left, blackNum, refNum)
			&& Check(root->_right, blackNum, refNum);
	}

测试代码:

void TestRBTree1()
{
	int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13,8, 3, 1, 10, 6, 4, 7, 14, 13 };
	//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14,4, 2, 6, 1, 3, 5, 15, 7, 16, 14, };

	RBTree<int, int> t1;
	for (auto e : a)
	{
		t1.Insert({e, e});

	}
	t1.InOrder();
	cout << t1.IsBalance() << endl;
}
void TestRBTree2()
{
	cout << "Starting TestAVLTree2..." << endl; // 添加调试输出
	const int N = 10000;
	vector<int> v;
	v.reserve(N);
	srand(rand());

	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand());
		//cout << v.back() << endl;
	}

	size_t begin2 = clock();
	RBTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
		//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
	}
	size_t end2 = clock();

	cout << "Insert:" << end2 - begin2 << endl;
	cout << t.IsBalance() << endl;

	//cout << "Height:" << t.Height() << endl;

}

红黑树与AVL树的比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O($log_2 N$),红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。
 

红黑树的应用

1. C++ STL库 -- map/set、mutil_map/mutil_set

2. Java 库

3. linux内核

4. 其他一些库


结语:

       随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。

  

         在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。 

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值