C++ 红黑树

上一节我介绍了二叉搜索树家族的AVL树,这里我们来介绍二叉搜索树家族的另一个成员,也是使用最广泛的成员。

1.AVL树与红黑树的区别

  • 平衡性质
    • AVL 树:是严格的平衡二叉树,要求任意节点的左右子树高度差的绝对值不超过 1,能保证树的高度始终保持在O(logn)级别,查询效率非常稳定。
    • 红黑树:是一种弱平衡的二叉树,通过节点颜色和一些规则来保证从根节点到叶子节点的最长路径不超过最短路径的 2 倍,虽然不能像 AVL 树那样严格控制高度,但在实际应用中也能提供较为高效的操作。
  • 插入和删除操作
    • AVL 树:插入和删除节点后,为了保持平衡,可能需要进行多次旋转操作,调整的频率相对较高,导致其插入和删除操作的时间复杂度相对不稳定。
    • 红黑树:插入和删除操作相对简单,因为它的平衡条件相对宽松,所以在插入和删除时需要进行的调整操作通常比 AVL 树少,这使得红黑树在频繁进行插入和删除操作的场景下,效率可能更高。
  • 空间复杂度
    • AVL 树:节点结构相对简单,通常只需要存储数据和左右子节点的指针,不需要额外的空间来存储颜色等信息,所以空间复杂度相对较低。
    • 红黑树:由于需要为每个节点设置颜色属性,因此每个节点需要额外的存储空间来存储颜色信息,这使得红黑树的空间复杂度相对 AVL 树略高。
  • 应用场景
    • AVL 树:适用于查询操作非常频繁,对查询效率要求极高,且数据相对稳定,插入和删除操作较少的场景,如数据库索引、编译器的符号表等。
    • 红黑树:在插入和删除操作比较频繁,同时对查询效率也有一定要求的场景中表现出色,如 STL 中的 map、set 等容器,以及 Linux 内核中的进程调度等。

2.红黑树的概念

红⿊树是⼀棵⼆叉搜索树,他的每个结点增加⼀个存储位来表⽰结点的颜⾊,可以是红⾊或者⿊⾊。 通过对任何⼀条从根到叶⼦的路径上各个结点的颜⾊进⾏约束,红⿊树确保没有⼀条路径会⽐其他路径⻓出2倍,因⽽是接近平衡的。

2.1 红黑树的规则

为了完成红黑树的功能,设计者设计了以下规则来控制

  1. 每个结点不是红⾊就是⿊⾊
  2. 根结点是⿊⾊的
  3. 如果⼀个结点是红⾊的,则它的两个孩⼦结点必须是⿊⾊的,也就是说任意⼀条路径不会有连续的
    红⾊结点。
  4. 对于任意⼀个结点,从该结点到其所有NULL结点的简单路径上,均包含相同数量的⿊⾊结点


• 由规则4可知,从根到NULL结点的每条路径都有相同数量的⿊⾊结点,所以极端场景下,最短路径 就就是全是⿊⾊结点的路径,假设最短路径⻓度为bh(black height)。
• 由规则2和规则3可知,任意⼀条路径不会有连续的红⾊结点,所以极端场景下,最⻓的路径就是⼀⿊⼀红间隔组成,那么最⻓路径的⻓度为2*bh。
• 综合红⿊树的4点规则⽽⾔,理论上的全⿊最短路径和⼀⿊⼀红的最⻓路径并不是在每棵红⿊树都存在的。假设任意⼀条从根到NULL结点路径的⻓度为x,那么bh <= h <= 2*bh。

2.2红黑树的效率

假设N是红⿊树树中结点数量,h最短路径的⻓度,那么2 ^h − 1 <= N < 2^( 2∗h) − 1 , 由此推出hlogN ,也就是意味着红⿊树增删查改最坏也就是⾛最⻓路径2 ∗ logN ,那么时间复杂度还是 O( logN )。
红⿊树的表达相对AVL树要抽象⼀些,AVL树通过⾼度差直观的控制了平衡。红⿊树通过4条规则的颜 ⾊约束,间接的实现了近似平衡,他们效率都是同⼀档次,但是相对⽽⾔,插⼊相同数量的结点,红⿊树的旋转次数是更少的,因为他对平衡的控制没那么严格。

3.红黑树的实现

3.1红黑树的结构

cpp 复制代码
enum Colour
{
	RED,
    BLACK
};
template<class K,class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode* _left;
	RBTreeNode* _right;
	RBTreeNode* _parent;
	Colour _col;

	RBTreeNode(const pair<K,V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
	{}
};

template<class K,class V>
class RBTree
{
	typedef RBTreeNode<K,V> Node;
public:
private:
	Node* _root = nullptr;
};

3.2 红黑树的插入

3.2.1红⿊树树插⼊⼀个值的⼤概过程

  1. 插⼊⼀个值按⼆叉搜索树规则进⾏插⼊,插⼊后我们只需要观察是否符合红⿊树的4条规则。
  2. 如果是空树插⼊,新增结点是⿊⾊结点。如果是⾮空树插⼊,新增结点必须红⾊结点,因为⾮空树插⼊,新增⿊⾊结点就破坏了规则4,规则4是很难维护的。
  3. ⾮空树插⼊后,新增结点必须红⾊结点,如果⽗亲结点是⿊⾊的,则没有违反任何规则,插⼊结束。
  4. ⾮空树插⼊后,新增结点必须红⾊结点,如果⽗亲结点是红⾊的,则违反规则3。进⼀步分析,c是 红⾊,p为红,g必为⿊,这三个颜⾊都固定了,关键的变化看u的情况,需要根据u分为以下⼏种情况分别处理。

3.2.2情况1:变⾊

c为红,p为红,g为⿊,u存在且为红,则将p和u变⿊,g变红。在把g当做新的c,继续往上更新。
分析:因为p和u都是红⾊,g是⿊⾊,把p和u变⿊,左边⼦树路径各增加⼀个⿊⾊结点,g再变红,相 当于保持g所在⼦树的⿊⾊结点的数量不变,同时解决了c和p连续红⾊结点的问题,需要继续往上更新 是因为,g是红⾊,如果g的⽗亲还是红⾊,那么就还需要继续处理;如果g的⽗亲是⿊⾊,则处理结束了;如果g就是整棵树的根,再把g变回⿊⾊。
情况1只变⾊,不旋转。所以⽆论c是p的左还是右,p是g的左还是右,都是上⾯的变⾊处理⽅式。

3.2.3情况2:单旋+变⾊

c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。 分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解决问题,需要旋转+变⾊。

3.2.4情况2:双旋+变⾊

c为红,p为红,g为⿊,u不存在或者u存在且为⿊,u不存在,则c⼀定是新增结点,u存在且为⿊,则 c⼀定不是新增,c之前是⿊⾊的,是在c的⼦树中插⼊,符合情况1,变⾊将c从⿊⾊变成红⾊,更新上来的。 分析:p必须变⿊,才能解决,连续红⾊结点的问题,u不存在或者是⿊⾊的,这⾥单纯的变⾊⽆法解决问题,需要旋转+变⾊。

3.3红⿊树的插⼊代码实现

cpp 复制代码
#pragma once

enum Colour
{
	RED,
    BLACK
};
template<class K,class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode* _left;
	RBTreeNode* _right;
	RBTreeNode* _parent;
	Colour _col;

	RBTreeNode(const pair<K,V>& kv)
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
	{}
};

template<class K,class V>
class RBTree
{
	typedef RBTreeNode<K,V> Node;
public:
	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)
		{
			parent = cur;
			if (cur->_kv.first > kv.first)
			{
				cur = cur->_left;
			}
			else if(cur->_kv.first<kv.first)
			{
				cur = cur->_right;
			}
			else
			{
				return false;
			}
		}

		cur = new Node(kv);
		cur->_parent = parent;
		cur->_col = RED;
		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}

		//控制颜色的平衡
		while (parent && parent->_col == RED)
		{
			Node* grandparent = parent->_parent;
			Node* uncle;
			if (parent == grandparent->_left)
			{
				uncle = grandparent->_right;
			}
			else
			{
				uncle = grandparent->_left;
			}

			//uncle存在且为红色
			if(uncle && uncle->_col == RED)
			{
				grandparent->_col = RED;
				uncle->_col = BLACK;
				parent->_col = BLACK;
				
				cur = grandparent;
			}
			else 
			{
				if (parent == grandparent->_left)
				{
					if(cur == parent->_left)
					{
						RotateR(grandparent);
						grandparent->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateLR(grandparent);
						cur->_col = BLACK;
						grandparent->_col = RED;
					}
					break;
				}
				else
				{
					if (cur == parent->_right)
					{
						RotateL(grandparent);
						grandparent->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateRL(grandparent);
						cur->_col = BLACK;
						grandparent->_col = RED;
					}
					break;
				}
	
			}
		}
		_root->_col = BLACK;
		return true;
	}

	Node* Find(const K& key)
	{
		Node* cur = _root;
			while (cur)
			{
				if (cur->_kv.first < key)
				{
					cur = cur->_right;
				}
				else if (cur->_kv.first > key)
				{
					cur = cur->_left;
				}
				else
				{
					return cur;
				}
			}
		return nullptr;
	}

	void InOrder()
	{
		_InOrder(_root);
	}
	void _InOrder(const Node* root)
	{
		if (root == nullptr)
			return;

		_InOrder(root->_left);
		cout << root->_kv.first << ' ';
		_InOrder(root->_right);

	}

	void RotateRL(Node* parent)
	{
	
		Node* parentR = parent->_right;
		Node* parentRL = parentR->_left;
		RotateR(parent->_right);
		RotateL(parent);
	}
	void RotateLR(Node* parent)
	{

		Node* parentL = parent->_left;
		Node* parentLR = parentL->_right;
		RotateL(parent->_left);
		RotateR(parent);

	}
	void RotateR(Node* parent)
	{
		Node* parentL = parent->_left;
		Node* parentLR = parentL->_right;
		Node* parentPa = parent->_parent;

		if (parentPa)
		{
			if (parent == parentPa->_left)
				parentPa->_left = parentL;
			else
				parentPa->_right = parentL;
		}

		if (parentLR)
			parentLR->_parent = parent;

		parent->_parent = parentL;
		parent->_left = parentLR;

		parentL->_right = parent;
		parentL->_parent = parentPa;

	}
	void RotateL(Node* parent)
	{
		Node* parentR = parent->_right;
		Node* parentRL = parentR->_left;
		Node* parentPa = parent->_parent;

		if (parentPa)
		{
			if (parent == parentPa->_left)
				parentPa->_left = parentR;
			else
				parentPa->_right = parentR;
		}

		if (parentRL)
			parentRL->_parent = parent;

		parent->_parent = parentR;
		parent->_right = parentRL;

		parentR->_parent = parentPa;
		parentR->_left = parent;
	}

private:
	Node* _root = nullptr;
};

3.4红⿊树的查找

cpp 复制代码
Node* Find(const K& key)
{
	Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)
			{
				cur = cur->_right;
			}
			else if (cur->_kv.first > key)
			{
				cur = cur->_left;
			}
			else
			{
				return cur;
			}
		}
	return nullptr;
}

3.5红⿊树的验证

这⾥获取最⻓路径和最短路径,检查最⻓路径不超过最短路径的2倍是不可⾏的,因为就算满⾜这个条件,红⿊树也可能颜⾊不满⾜规则,当前暂时没出问题,后续继续插⼊还是会出问题的。所以我们还是去检查4点规则,满⾜这4点规则,⼀定能保证最⻓路径不超过最短路径的2倍。

  1. 规则1枚举颜⾊类型,天然实现保证了颜⾊不是⿊⾊就是红⾊。
  2. 规则2直接检查根即可
  3. 规则3前序遍历检查,遇到红⾊结点查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲的颜⾊就⽅便多了。
  4. 规则4前序遍历,遍历过程中⽤形参记录跟到当前结点的blackNum(⿊⾊结点数量),前序遍历遇到⿊⾊结点就++blackNum,⾛到空就计算出了⼀条路径的⿊⾊结点数量。再任意⼀条路径⿊⾊结点数量作为参考值,依次⽐较即可。
cpp 复制代码
bool IsRBTree()
{
	return _IsRBTree(_root);
}
bool _IsRBTree(const Node* root)
{
	if (root == nullptr)
		return true;
	if (root->_col == RED)
		return false;

	Node* cur = root;
	int RetNum = 0;
	while (cur)
	{
		if (cur->_col == BLACK)
			RetNum++;
		cur = cur->_left;
	}
	return Check(root, 0, RetNum);
}

bool Check(const Node*& root,int BlackNum,int refNum)
{
	if (root == nullptr)
	{
		if (BlackNum != refNum)
		{
			cout << "黑色节点数不匹配" << endl;
			return false;
		}
		return true;
	}

	if (root->_col == RED && root->_parent->_col == BLACK)
	{
		cout << "红色节点重复" << endl;
		return false;
	}
	if (root->_col == BLACK)
	{
		BlackNum++;
	}
	return Check(root->_left, BlackNum, refNum) && Check(root->_right, BlackNum, refNum);
}
相关推荐
Kairo_0133 分钟前
在 API 模拟阶段:Apipost vs. Faker.js vs. Postman —— 为什么 Apipost 是最优选择
开发语言·javascript·postman
Once_day44 分钟前
研发效率破局之道阅读总结(4)个人效率
开发语言·研发效能·devops
xcLeigh44 分钟前
HTML5好看的水果蔬菜在线商城网站源码系列模板8
java·前端·html5
痕51744 分钟前
如何在idea中写spark程序。
开发语言
Alsn861 小时前
11.Spring Boot 3.1.5 中使用 SpringDoc OpenAPI(替代 Swagger)生成 API 文档
java·spring boot·后端
liyongjun63161 小时前
Java List分页工具
java·后端
橙子199110161 小时前
请简述一下什么是 Kotlin?它有哪些特性?
android·开发语言·kotlin
猎人everest2 小时前
Spring Boot集成Spring Cloud 2024(不使用Feign)
java·spring boot·spring cloud
jiunian_cn2 小时前
【c++】【STL】list详解
数据结构·c++·windows·list·visual studio
虾球xz2 小时前
游戏引擎学习第250天:# 清理DEBUG GUID
c++·学习·游戏引擎