【平衡二叉搜索树】红黑树

图片名称
🎉博主首页: 有趣的中国人

🎉专栏首页: C++进阶


在这里插入图片描述

小伙伴们大家好,本片文章将会讲解平衡二叉搜索树红黑树的相关内容。


如果看到最后您觉得这篇文章写得不错,有所收获,麻烦点赞👍、收藏🌟、留下评论📝。您的支持是我最大的动力,让我们一起努力,共同成长!



1. 何为红黑树


🧐红黑树的定义

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

它在插入和删除节点时能够保持树的平衡,从而保证了搜索、插入和删除等操作的时间复杂度为 O ( l o g n ) O(log n) O(logn)

在这里插入图片描述


🧐红黑树的性质

1、 每个节点不是红色就是黑色;

2、 根节点的颜色是黑色的;

3、不能出现连续的红色节点;

4、每条路径上拥有相同数量的黑色节点;

5、每个叶子节点的颜色是黑色的(这里的叶子节点指的是空节点)。


问题来了,为什么满足以上条件就能保证最长路径不会超过最短路径的两倍呢?

主要看3、4两个条件,那么最短路径肯定就是连续的黑色节点,最长路径就是一黑一红交替出现,假设黑色节点的数量是n,最短路径的长度就是n,最长路径的长度就是2n。(最短和最长可能在红黑树中不会出现)



2. 关于红黑树的插入



🧐红黑树插入节点的初始颜色

我们插入节点一定也要满足红黑树的5个条件,尤其是第三条和第四条,即:

  1. 每条简单路径上的黑色节点数量相同;
  2. 不能出现连续的红色节点。
    • 即一个红色节点的孩子节点只能是黑色节点。

那么如果我们: 插入的是 黑色 节点,就会导致此条路径上的黑色节点数量增加,但是其他路径上的黑色节点数量没有变化,非常难以调节。

但是如果我们: 插入的是 红色 节点只需要判断一下它的父亲节点是否是红色 ,如果是红色,就继续向上调节,如果不是红色(黑色),说明不需要调节,直接插入即可。


🧐红黑树插入节点时的调节

据上所述,插入的节点是红色节点,只有当父亲也是红色节点的时候才要进行调整,那么调整分为以下几种情况(主要看叔叔):

首先约定当前节点为cur,父亲节点为parent,祖父节点为grandparent,叔叔节点为uncle

🏄🏼‍♂️情况1:叔叔存在且为红色

1、uncle在grandparent的右边,cur在parent的左边:

在这里插入图片描述

我们调节的时候一定要保证满足红黑树的条件, 一定不要改变各个路径中的黑色节点的个数。

由于 不能出现两个连续的红色节点,所以我们要把:

1、 parent节点的颜色改为 黑色(导致父亲所在路径黑色数量增加);

2、把grandparent节点的颜色变为 红色(解决1的问题,引发uncle节点所在路径黑色节点数目减少);

3、把uncle节点颜色 变红(解决2的问题)。

4、由于根节点变为红色,所以还要继续向上调整,因为grandparentparent节点可能也是红色。

5、如果调节到根节点,把根节点变为黑色。

这样调整咱们就满足了红黑树的条件,图解如下:

图片名称

除此之外,可能还有以下几种情况,但解决方法都相同:

2、uncle在grandparent的右边,cur在parent的右边:

图片名称

3、 uncle在grandparent的左边,cur在parent的右边:

图片名称

4、uncle在grandparent的左边,cur在parent的左边:

图片名称




🏄🏼‍♂️情况2:叔叔不存在或者存在为黑色

1、 uncle在grandparent的左边,cur在parent的左边:

在这里插入图片描述

我们调节的时候一定要保证满足红黑树的条件, 一定不要改变各个路径中的黑色节点的个数。

由于 不能出现两个连续的红色节点,所以我们要把:

1、 parent节点的颜色改为 黑色(导致父亲所在路径黑色数量增加);

2、把grandparent节点的颜色变为 红色(解决1的问题,引发uncle节点所在路径黑色节点数目减少);

3、将grandparent节点进行一次右旋转(上图情况为右旋转);

在这里插入图片描述

除此之外,可能还有以下几种情况,但解决方法类似(旋转方向、次数不同):


2、 uncle在grandparent的右边,cur在parent的右边:

此时就不是单旋那么简单了,我们要把这种情况首先变成上面的那种情况,所以首先要对parent进行左单旋,单旋之后cur变成了上面情况parent所在的位置,因此要cur的颜色变为黑色,之后的操作就和上面的情况一样了。

先对parent进行左单旋,再对grandparent进行右单旋

在这里插入图片描述

3、uncle在grandparent的左边,cur在parent的左边:

变色之后,旋转时:先对parent进行右单旋,再对grandparent进行左单旋


在这里插入图片描述

4、 uncle在grandparent的左边,cur在parent的右边:


这种情况变色之后直接对grandparent左单旋。


在这里插入图片描述



3. C++模拟实现红黑树


🏄🏼‍♂️节点类型定义

// 枚举类型定义颜色
enum Colour
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTNode
{
	typedef RBTNode<K, V> Node;
	Node* _left;
	Node* _right;
	Node* _parent;
	pair<K, V> _kv;
	Colour _col;
	RBTNode(const pair<K, V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_col(RED)// 初始给红色,上面讲过
	{}
};


🏄🏼‍♂️插入的模拟实现

bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (kv.first > cur->_kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else if (kv.first < cur->_kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}
	cur = new Node(kv);
	if (kv.first > parent->_kv.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;
	while (parent && parent->_col == RED)
	{
		Node* grandparent = parent->_parent;
		// 父亲在祖父的左边,祖父的右边是叔叔
		if (parent == grandparent->_left)
		{
			Node* uncle = grandparent->_right;
			// 叔叔存在且为红
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandparent->_col = RED;
				// 继续向上判断
				cur = grandparent;
				parent = cur->_parent;
			}
			// 叔叔不存在 或者 存在且为黑
			else
			{
				// cur 在父亲左边
				if (cur == parent->_left)
				{
//       g(B)                g(R)   
//   p(R)     u(B)  =>   p(B)     u(B)
// c(R)               c(R)
					parent->_col = BLACK;
					grandparent->_col = RED;
					RotateR(grandparent);
				}
				// cur 在父亲右边
				else
				{
//       g(B)               g(B)   
//   p(R)     u(B)  =>   c(R)   u(B) 
//		 c(R)          p(R) 		
					cur->_col = BLACK;
					grandparent->_col = RED;
					RotateL(parent);
					RotateR(grandparent);
				}
				break;
			}
		}
		// 父亲在祖父的右边,祖父的左边是叔叔
		else
		{
			Node* uncle = grandparent->_left;
			// 叔叔存在且为红
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandparent->_col = RED;

				cur = grandparent;
				parent = cur->_parent;
			}
			// 叔叔不存在,或者存在且为黑
			else
			{
				// cur 在父亲右边
				if (cur == parent->_right)
				{
//    g(B)                   g(R)                    p(B)
// u(B)   p(R)            u(B)   p(B)             g(R) cur(R)
//          cur(R)                 cur(R)       u(B)  
					parent->_col = BLACK;
					grandparent->_col = RED;
					RotateL(grandparent);
				}
				// cur 在父亲左边
				else
				{
//    g(B)                    g(B)                   cur(B) 
// u(B)   p(R)            u(B)   cur(R)           g(R)  cur(R)
//    cur(R)                         p(R)       u(B)
					cur->_col = BLACK;
					grandparent->_col = RED;
					RotateR(parent);
					RotateL(grandparent);
				}
				break;
			}
		}
	}
	// 如果更新到根节点,更新为黑色
	_root->_col = BLACK;
	return true;
}

🏄🏼‍♂️左单旋和右单旋

这里有问题的可以看以下博主的上一篇关于AVL树的文章,里面有详细的讲解

void RotateR(Node* root)
{
	Node* subL = root->_left;
	Node* subLR = subL->_right;
	root->_left = subLR;
	if (subLR)
		subLR->_parent = root;
	subL->_right = root;
	Node* ppNode = root->_parent;
	root->_parent = subL;
	if (ppNode)
	{
		if (ppNode->_left == root)
		{
			ppNode->_left = subL;
		}
		else
		{
			ppNode->_right = subL;
		}
		subL->_parent = ppNode;
	}
	else
	{
		_root = subL;
		subL->_parent = nullptr;
	}
}

void RotateL(Node* root)
{
	Node* subR = root->_right;
	Node* subRL = subR->_left;
	root->_right = subRL;
	if (subRL)
		subRL->_parent = root;
	subR->_left = root;
	Node* ppNode = root->_parent;
	root->_parent = subR;
	if (ppNode)
	{
		if (ppNode->_left == root)
		{
			ppNode->_left = subR;
		}
		else
		{
			ppNode->_right = subR;
		}
		subR->_parent = ppNode;
	}
	else
	{
		_root = subR;
		subR->_parent = nullptr;
	}
}

🏄🏼‍♂️测试

void RBTest()
{
	//int a[] = {16, 3, 7, 11, 9, 26, 18, 14, 15};
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14,16, 
		3, 7, 11, 9, 26, 18, 14, 15 };
	RBTree<int, int> t;
	for (auto& e : a)
	{
		t.Insert({ e,e });
	}
	t.InOrder();
}

在这里插入图片描述



4. 检测函数


检查的时候无非就是看一下是否满足红黑树的几个条件,最重要的就是三、四两点,思路如下:

1、看一下根节点是否为红色(条件2的检查);
2、对于一个节点如果是红色,看一下它的父亲节点是否为红色(条件3的检查);

最难的是条件4,如何检查每条路径上的黑色节点数量相同呢?

可以采用 深度优先(DFS 的思路:

1、先算一条路径的黑色节点数(比如最左路径);
2、然后把这个值传入到Check函数;
3、每当指针走到空的时候都判断以下和这个参考值是否相同,如果不同说明有错。

public:
bool IsBalance()
{
	// 判断根节点颜色
	if (_root->_col == RED)
	{
		return false;
	}

	int refNum = 0;
	Node* cur = _root;
	// 算一条路径的黑色节点数
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			++refNum;
		}

		cur = cur->_left;
	}

	return Check(_root, 0, refNum);
}

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;
	}
	// 判断是否有连续的红色节点
	if (root->_col == RED && root->_parent->_col == RED)
	{
		cout << root->_kv.first << "存在连续的红色节点" << endl;
		return false;
	}
	// 如果为黑色,数量加1
	if (root->_col == BLACK)
	{
		blackNum++;
	}
	// 递归调用
	return Check(root->_left, blackNum, refNum)
		&& Check(root->_right, blackNum, refNum);
}

在这里插入图片描述



5. 完整代码


🎉博主gitee链接: 红黑树完整代码

有需要的小伙伴自取哈:

在这里插入图片描述

  • 34
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 38
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 38
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值