C++红黑树

Posted beyond->myself

tags:

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

红黑树

文章目录

一、红黑树的引入

有了二叉搜索树,为什么还需要平衡二叉树?

  • 在学习二叉搜索树、平衡二叉树时,我们不止一次提到,二叉搜索树容易退化成一条链
  • 这时,查找的时间复杂度从O(log2N) 也将退化为O(N)
  • 引入对左右子树高度差有限制的平衡二叉树,保证查找操作的最坏时间复杂度也为O(log2N)

有了平衡二叉树,为什么还需要红黑树?

  • AVL的左右子树高度差不能超过1,每次进行插入/删除操作时,几乎都需要通过旋转操作保持平衡
  • 在频繁进行插入/删除的场景中,频繁的旋转操作使得AVL的性能大打折扣
  • 红黑树通过牺牲严格的平衡,换取插入/删除时少量的旋转操作,整体性能优于AVL
  • 红黑树插入时的不平衡,不超过两次旋转就可以解决;删除时的不平衡,不超过三次旋转就能解决
  • 红黑树的红黑规则,保证最坏的情况下,也能在O(log2N)时间内完成查找操作

二、红黑树的概念

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

同是二叉搜索平衡树,但是AVL树控制的比红黑树严格的多,AVL树要是每个节点的平衡因子绝对值不超过1,就会导致不断的去旋转调整,付出相对较高的代价,而这里红黑树更像是一种近似平衡,条件没有这么苛刻。

如下一棵树,站在红黑树的角度看是平衡的,站在AVL树的角度看就是不平衡的,需要旋转调整:

但是从搜索效率的角度看AVL树还是好一点,因为它的平衡标准高,就导致其更加平衡,相同数量的节点情况下AVL树的高度会更低,加上存100w个数据,AVL树大概有20层(log100w),而红黑树最坏就能达到40层,显然AVL树的搜索效率高。但是在内存里找20次和找40次没有什么区别,因为CPU足够的快,这里简单提一下。


三、红黑树的性质

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

根据这些规则,红黑树是如何保证最长路径不超过最短路径的2倍的呢?

首先我们根据规则分析得知,我们假设一条路径的黑色节点的个数为N个,则最长路径和最短路径的情况如下:

  • 最短路径:全黑

  • 最长路径:一黑一红间隔

而这里一黑一红间隔的原因在于红黑树不允许出现连续的红节点,为了能最大程度的保证最长节点数,唯有一黑一红间隔的方式才能达到最长,综上当黑节点个数固定为N时,最短路径节点个数为N,最长路径节点个数为2N


四、红黑树节点的定义

这里节点的实现相较于AVL树我们依旧是创建成KV模型、三叉链结构,唯一有所改变的是这里要通过枚举的方式把红色和黑色定义好,并在节点类内部定义变量_col表示节点颜色,最后记得写上构造函数。

enum Colour

	RED,
	BLOCK,
;
//节点类
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)
	
;

为什么插入的节点在构造函数这里要处理成红色?

  • 如果处理成黑色,则一定导致新插入节点的那条路径多出一个黑色节点,不再满足各个路径黑色节点个数相同的性质,一定破坏性质4,此时很难维护。
  • 如果处理成红色,则可能父亲节点也是红色,此时就出现了连续的红色节点,破坏性质3,不过此时我们向上调整即可,但如果父亲节点是黑色,那就无需操作了,不违反任何性质。

综合利弊,插入黑色节点一定会破坏性质4,而插入红色节点可能破坏性质3,因此处理成红色为宜。


五、红黑树类的基本框架

此模板类主要是用于红黑树的插入、旋转、调整、验证等等操作,基本框架如下:

//红黑树的类
template <class K, class V>
class RBTree

	typedef RBTreeNode<K, V> Node;
public:
// 插入
	bool Insert(const pair<K, V>& kv);
// 中序遍历
	void InOrder();
// 查找函数
	Node* Find(const K& key);
// 判断是否为平衡树
    bool IsBalanceTree();
  
private:
 void _InOrder(Node* root);
 bool _IsValidRBTree(Node* pRoot, size_t k, const size_t blackCount);
private:
	Node* _root = nullptr;
;

六、红黑树的插入

红黑树的插入操作主要分为这几大步骤:

  • 1、一开始为空树,直接new新节点
  • 2、一开始非空树,寻找插入的合适位置
  • 3、找到插入的合适位置后,进行父亲与孩子的双向链接
  • 4、检测新节点插入后,红黑树的性质是否造到破坏
  • 5、调整节点的颜色和位置,保持红黑树的性质不被破坏

接下来对其进行逐个分析:

  • 1、一开始为空树,直接new新节点:

因为树为空的,所以直接new一个新插入的节点,将其作为根_ root即可,接着更新颜色_col为黑色。

  • 2、一开始非空树,寻找插入的合适位置:

这里和二叉搜索树的寻找合适的插入位置的思想一样,都要遵循以下几步:

  1. 插入的值 > 节点的值,更新到右子树查找
  2. 插入的值 < 节点的值,更新到左子树查找
  3. 插入的值 = 节点的值,数据冗余插入失败,返回false

当循环结束的时候,就说明已经找到插入的合适位置,即可进行下一步链接。

  • 3、找到插入的合适位置后,进行父亲与孩子的双向链接:

注意这里节点的构成为三叉链,因此最后链接后端孩子和父亲是双向链接,具体操作如下:

  1. 插入的值 > 父亲的值,把插入的值链接在父亲的右边
  2. 插入的值 < 父亲的值,把插入的值链接在父亲的左边
  3. 因为是三叉链结构,插入后记得双向链接(孩子链接父亲)

走到这,说明节点已经插入完毕,接下来就要对红黑树的颜色进行调整了

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

不是所有的情况都是需要进行调整的,当**插入节点的父亲为黑色(新节点的默认颜色是红色),那么就不需要进行调整,因为没有破坏红黑树的任何一条性质**。

只有当插入节点的父亲为红色时(新节点的默认颜色也是是红色),才需要进行调整,因为此时插入的节点和父亲都是红色节点,但是红黑树不允许出现连续的红色节点,此时就要进行调整

注意这里既然插入节点cur的父亲p是红色,那么根据红黑树的性质(根结点是黑色的),其父亲的父亲g也就是祖父必然存在且一定是黑色,那么其父亲的兄弟节点u(可能不存在)也就是新插入节点cur的叔叔。因此我们约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点

  • 5、这里调整的办法主要是看叔叔节点的颜色如何,叔叔节点的不同,会导致三种不同的情况需要调整:
  • 情况一:cur为红,p为红,g为黑,u存在且为红
  • 情况二:cur为红,p为红,g为黑,u不存在
  • 情况三:cur为红,p为红,g为黑,u存在且为黑

接下来分别进行讨论:

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

为了避免出现连续的红色节点,我们可以把父节点p变黑,但是为了保证每条路径的黑色节点个数相同,我们需要把祖父节g点变红(不影响其它路径黑节点的个数),再把叔叔节点u变黑。

a、b、c、d、e不存在

a、b、c、d、e存在

调整并未结束,此时祖父节点g为红色,但是如果这棵树本就是一颗完整的树呢?也就是g为根节点,那么只需要把节点g变成黑色即可。

如果这棵树是一棵树的子树,那么刚好把祖父节点g作为新插入的节点cur向上继续调整(继续判断父亲、叔叔如何……),直至调整结束。

  • 补充**:**情况一不关心左右关系,只变色不旋转,所以 p、u是g的左或右是无所谓的,cur是p的左或右也是无所谓的。
  • 说明:u的情况有两种
  1. 如果节点u不存在,则cur一定是新插入节点,因为如果cur不是新插入节点,则cur和p一定有一个节点的颜色是黑色,就不满足性质4:每条路径黑色节点个数相同。
  2. 如果节点u存在,则其一定是黑色的,那么cur节点原来的颜色一定是黑色的,现在看到其是红色的原因是因为cur的子树在调整的过程中将cur节点的颜色由黑色改成红色。

接下来分析情况2:

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

重要结论u不存在,那么a、b、c、d、e都不存在

此时就是一个很经典的右单旋结构(新节点插入较高左子树的左侧)我们可以先对其进行一个右单旋,再来更新颜色。具体步骤如下:

  1. 让祖父g变成父亲p的右子树
  2. 父亲p作为根节点
  3. 更新父亲节点p为黑色
  4. 更新祖父g为红色

  • 补充:

如若p为g的右孩子,cur为p的右孩子,则针对p做左单旋转,示例:

如若祖孙三代的关系是折线(cur、parent、grandfather这三个结点为一条折线),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理。示例:

左右双旋

右左双旋

综上:

  1. p为g的左,cur为p的左,则进行右单旋 + p变黑,g变红
  2. p为g的右,cur为p的右,则进行左单旋 + p变黑,g变红
  3. p是g的左,cur是p的右,则进行左右双旋 + cur变黑, g变红
  4. p是g的右,cur是p的左,则进行右左双旋 + cur变黑, g变红

下面进入情况三

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

此情况绝非单独存在,绝不可能是真的新节点cur插入,然后还会出现p为红,g为黑,u存在且为黑的情况,如果存在,那么只能说明先前插入节点或者构造函数就有问题,因为插入前就不符合红黑树的性质(每个路径的黑节点个数均相同)

既然情况三出现了,那么一定是合理的,它就是建立在情况一的基础上继续往上调整从而出现的一种特殊情况,具体咱就是画图演示:

此时就是很明显的一个情况3了,cur为红,pp为红,gg为黑,u存在且为黑,由此证明,情况三是通过情况一向上继续调整演化出来的。并且此新节点一定是从p和x任意一颗左右子树插入或演化上来的,才引发后续的cur从黑变红。

此时就是一个很经典的右单旋结构(cur在较高左子树的左侧)我们可以先对其进行一个右单旋,再来更新颜色。具体步骤如下:

  1. 让p的右子树变成g的左子树
  2. 让p变成根节点位置
  3. p的右子树指向g
  4. 更新p的颜色为黑色
  5. 更新g的颜色为红色

  • 补充:

如若p为g的右孩子,cur为p的右孩子,则进行左单旋 + 调色,示例:

若祖孙三代的关系是折现(cur、parent、grandfather这三个结点为一条折线),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理。示例:

左右双旋

右左双旋

综上:

  1. p为g的左,cur为p的左,则进行右单旋 + p变黑,g变红
  2. p为g的右,cur为p的右,则进行左单旋 + p变黑,g变红
  3. p是g的左,cur是p的右,则进行左右双旋 + cur变黑, g变红
  4. p是g的右,cur是p的左,则进行右左双旋 + cur变黑, g变红

情况二和情况三旋转 + 变色后,这颗子树不违反红黑树规则,相比插入前,且黑色节点的数量不变,不会影响上层,处理结束了。

代码如下:

bool Insert(const pair<K, V>& kv)

	//1、一开始为空树,直接new新节点
	if (_root == nullptr)
	
		_root = new Node(kv);
		_root->_col = BLACK;//新插入的节点处理成黑色
		return true;
	
	//2、寻找插入的合适位置
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	
		if (cur->_kv.first < kv.first)
		
			parent = cur;
			cur = cur->_right;//插入的值 > 节点的值,更新到右子树查找
		
		else if (cur->_kv.first > kv.first)
		
			parent = cur;
			cur = cur->_left;//插入的值 < 节点的值,更新到左子树查找
		
		else
		
			return false;//插入的值 = 节点的值,数据冗余插入失败,返回false
		
	
	//3、找到了插入的位置,进行父亲与插入节点的链接
	cur = new Node(kv);
	cur->_col = RED;//插入的节点处理成红色
	if (parent->_kv.first < kv.first)
	
		parent->_right = cur;//插入的值 > 父亲的值,链接在父亲的右边
	
	else
	
		parent->_left = cur;//插入的值 < 父亲的值,链接在父亲的左边
	
	cur->_parent = parent;//三叉链,要双向链接

//4、检测新节点插入后,红黑树的性质是否造到破坏
	while (parent && parent->_col == RED)//存在连续的红色节点
	
		Node* grandfather = parent->_parent;
		assert(grandfather);
		//先确保叔叔的位置
		if (grandfather->_left == parent)
		
			Node* uncle = grandfather->_right;
			//情况一:cur为红,p为红,g为黑,u存在且为红
			if (uncle && uncle->_col == RED)
			
				//变色
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
				//继续往上处理
				cur = grandfather;
				parent = cur->_parent;
			
			//情况二+情况三:叔叔不存在,或者叔叔存在且为黑
			else
			
				if (cur == parent->_left)//p为g的左,cur为p的左,则进行右单旋 + p变黑,g变红
				
					//		  g
					//     p
					// cur
					RotateR(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				
				else//p是g的左,cur是p的右,则进行左右双旋 + cur变黑, g变红
				
					//		  g
					//	 p
					//	     cur
					RotateLR(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				
				break;
			
		
		else//grandfather->_right == parent
		
			Node* uncle = grandfather->_left;
			//情况一:cur为红,p为红,g为黑,u存在且为红
			if (uncle && uncle->_col == RED)
			
				//变色
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
				//继续往上处理
				cur = grandfather;
				parent = cur->_parent;
			
			//情况二+情况三:叔叔不存在,或者叔叔存在且为黑
			else
			
				if (cur == parent->_right)//p为g的右,cur为p的右,则进行左单旋 + p变黑,g变红
				
					//	g
					//	   p
					//	     cur
					RotateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				
				else//p是g的右,cur是p的左,则进行右左双旋 + cur变黑, g变红
				
					//   g
					//	      p
					//	cur
					RotateRL(grandfather);
					cur->_col = BLACK;
					grandfather->_col = RED;
				
				break;
			
		
	
	_root->_col = BLACK;//暴力处理把根变成黑色
	return true;

//1、左单旋
void RotateL(Node* parent)

	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* ppNode = parent->_parent;//提前保持parent的父亲
	//1、建立parent和subRL之间的关系
	parent->_right = subRL;
	if (subRL)//防止subRL为空
	
		subRL->_parent = parent;
	
	//2、建立subR和parent之间的关系
	subR->_left = parent;
	parent->_parent = subR;
	//3、建立ppNode和subR之间的关系
	if (parent == _root)
	
		_root = subR;
		_root->_parent = nullptr;
	
	else
	
		if (parent == ppNode->_left)
		
			ppNode->_left = subR;
		
		else
		
			ppNode->_right = subR;
		
		subR->_parent = ppNode;//三叉链双向链接关系
	

//2、右单旋
void RotateR(Node* parent)

	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* ppNode = parent->_parent;
	//1、建立parent和subLR之间的关系
	parent->_left = subLR;
	if (subLR)
	
		subLR->_parent = parent;
	
	//2、建立subL和parent之间的关系
	subL->_right = parent;
	parent->_parent = subL;
	//3、建立ppNode和subL的关系
	if (parent == _root)
	
		_root = subL;
		_root->_parent = nullptr;
	
	else
	
		if (parent == ppNode->_left)
		
			ppNode->_left = subL;
		
		else
		
			ppNode->_right = subL;
		
		subL->_parent = ppNode;//三叉链双向关系
	

//3、左右双旋
void RotateLR(Node* parent)

	RotateL(parent->_left);
	RotateR(parent);

//4、右左双旋
void RotateRL(Node* parent)

	RotateR(parent->_right);
	RotateL(parent);

下面给出动图演示:

1.随即插入构建红黑树

2.以降序插入构建红黑树

3.以升序插入构建红黑树


七、红黑树的验证

红黑树的验证主要分为两大步骤:

  • 1、检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  • 2、检测其是否满足红黑树的性质

接下来分别演示:

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

这里只需要递归写一个中序遍历,并判断测试用例的结果是否为一个有序序列即可判断二叉搜索树:

//验证是否为一颗搜索二叉树
void InOrder()

	_InOrder(_root);//调用中序遍历子树
	cout << endl;

//中序遍历的子树
void _InOrder(Node* root)

	if (root == nullptr)
		return;
	_InOrder(root->_left);
	cout << root->_kv.first << " ";
	_InOrder(root->_right);

  • 2、检测其是否满足红黑树的性质:

这里只要判断是否满足红黑树的5大规则即可,具体操作如下:

  • 1、根节点是否为黑色
  • 2、任意一条路径黑色节点数是否相同(递归每一条和确定的一条比较是否相同)
  • 3、递归检测是否违反性质三从而出现连续的红节点
bool IsBalanceTree()

	Node* pRoot = _root;
	// 空树也是红黑树
	if (pRoot == nullptr)
		return true;
	// 检测根节点是否满足情况
	if (pRoot->_col != BLACK)
	
		cout << "违反红黑树性质二:根节点必须为黑色" << endl;
		return false;
	
	// 获取任意一条路径中黑色节点的个数-->拿最左路径作为比较基准值
	size_t blackCount = 0;
	Node* pCur = pRoot;
	while (pCur)
	
		if (pCur->_col == BLACK)
			blackCount++;
		pCur = pCur->_left;
	
	// 检测是否满足红黑树的性质,k用来记录路径中黑色节点的个数
	size_t k = 0;
	return _IsValidRBTree(pRoot, k, blackCount);

bool _IsValidRBTree(Node* pRoot, size_t k, const size_t blackCount)

	//走到null之后,判断k和black是否相等
	if (pRoot == nullptr)
	
		if (k != blackCount)
		
			cout << "违反性质四:每条路径中黑色节点的个数必须相同" << endl;
			return false;
		
		return true;
	
	// 统计黑色节点的个数
	if (pRoot->_col == BLACK)
		k++;
	// 检测当前节点与其双亲是否都为红色
	Node* pParent = pRoot->_parent;
	if (pParent && pParent->_col == Red && pRoot->_col == RED)
	
		cout << "违反性质三:没有连在一起的红色节点,而这里出现了" << endl;
		return false;
	
	return _IsValidRBTree(pRoot->_left, k, blackCount) 
        && _IsValidRBTree(pRoot->_right, k, blackCount);


八、红黑树的查找

红黑树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:

  • 1、若树为空树,则查找失败,返回nullptr。

  • 2、若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。

  • 3、若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。

  • 4、若key值等于当前结点的值,则查找成功,返回对应结点。

代码如下:

//查找函数
Node* Find(const K& key)

	Node* cur = _root;
	while (cur)
	
		if (key < cur->_kv.first) //key值小于该结点的值
		
			cur = cur->_left; //在该结点的左子树当中查找
		
		else if (key > cur->_kv.first) //key值大于该结点的值
		
			cur = cur->_right; //在该结点的右子树当中查找
		
		else //找到了目标结点
		
			return cur; //返回该结点
		
	
	return nullptr; //查找失败


九、红黑树的删除

红黑树的删除这里和AVL树一样就不做过多演示了,具体可参考《算法导论》或者《STL源码剖析》,也可参考此大佬的博文:红黑树的插入删除操作


十、红黑树与AVL树的比较

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


十一、红黑树的应用

  • Java中,TreeMap、TreeSet都使用红黑树作为底层数据结构
  • JDK 1.8开始,HashMap也引入了红黑树:当冲突的链表长度超过8时,自动转为红黑树
  • Linux底层的CFS进程调度算法中,vruntime使用红黑树进行存储。
  • 多路复用技术的Epoll,其核心结构是红黑树 + 双向链表。

参考文档:为什么这么多关于红黑树的面试题呢?

C++红黑树详解并封装实现map和set

红黑树详解


我们之前已经了解过了AVL树,那么接下来我们将介绍另一种二叉搜索树–红黑树。如果说AVL树是天才发现的,那么红黑树就是天才中的天才创造出来的。为什么这么说呢?接下里就随这篇文章一起来看看吧。

💡概念及定义

📕红黑树的概念

红黑树,顾名思义就是在每个结点上增加一个存储位表示结点颜色,其是一颗最长路径的长度不超过最短路径的长度的2倍的二叉搜索树,因而红黑树是近似平衡的。在红黑树中,为了便于规范,将空结点(NIL)认为是叶子节点,保证叶子结点一定是黑色的。

📕红黑树的性质

  • 1.每个结点不是红色就是黑色
  • 2.根节点是黑色的
    1. 如果一个结点是红色的,则它的两个孩子结点一定是黑色的
    1. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点
  • 5.每个叶子结点都是黑色的(需要注意的是,这里的叶子节点指的是空结点)
    由性质三知道红黑树中不存在连续的两个红色结点,由此推出在一条路径上红色结点的数量一定不会超过黑色结点的数量;而由性质四知道红黑树的每条路径上都有着相同数量的黑色结点。进一步的,我们可以得出结论:红黑树的最短路径为全是黑色结点,最长路径为全是红色结点。如此即可保证红黑树的最长路径不超过最短路径的两倍。
    ⏰【补充】路径的概念:树的路径为根节点到叶子结点的那条路径(但是在红黑树的概念理解中,我们将空结点认为是叶子结点)

📕红黑树的结点定义

与AVL树并无太大的不同,红黑树的结点只是在AVL树的基础上增加了颜色的定义(这里我们还是使用key-value模型的二叉搜索树),其中颜色用枚举表示:

enum Color

	BLACK,
	RED
;
template <class T>
//template <class K, class V>
struct RBTreeNode

	typedef RBTreeNode<K, V> Node;

	Node* _left;
	Node* _right;
	Node* _parent;
	//T _data;
	pair<K, V> _kv;
	Color _color;

	//RBTreeNode(const T& data)
	RBTreeNode(const pair<K, V> kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		//,_data(data)
		,_kv(kv)
		,_color(RED)
	
;

在这里结点颜色默认设置为红色,是为了方便后续插入,在保证红黑树性质的情况下,通过插入红色结点可以更好的保证性质四不被破坏,而如果插入黑色结点,那么保证每条路径的黑色结点数量相等操作将非常繁琐。

📕红黑树的结构

template <class K, class V>

class RBTree

public:
	//typedef RBTreeNode<T> Node;
	typedef RBTreeNode<K,V> Node;
	RBTree()
		:_root(nullptr)
	
private:
	Node* _root;
;

其实在STL中,红黑树的实现是还有一个header结点的,其中根节点的_parent指向header,header的_parent也指向根节点;而header的_left指向红黑树中最小的结点(最左下的结点),_right指向红黑树中最大的结点(最右下的结点),但为了简便,我们这里就不实现header结点的相关功能了。

📕红黑树的应用

红黑树最典型的应用就是实现C++STL中的map和set;其次还有Java库,Linux内核以及其他的一些内核都是用红黑树实现的。

📕红黑树与AVL树的比较

红黑树与AVL树都是平衡的二叉树:

  • AVL树是严格平衡的,而红黑树是近似平衡的
  • AVL树和红黑树的查找时间复杂度都是O(log2N)
  • 由于红黑树旋转次数更少,因此在增删过程中性能较优

💡插入操作

在AVL树的基础下,红黑树的插入操作其实就不难了。

🎄1、寻找要插入的位置

此步我们这钱已经多次介绍介绍,即若key值大于当前结点的key值,则向右寻找;若小于,则向左寻找;若相等,说明数据冗余,返回false。

🎄2.判断是否符合红黑树的规则

由于我们产生新结点时将其颜色设置为红色,而每条路径上黑色结点数量相等这条规则就交给调整时保证。
故判断是否为红黑树就转换为判断是否存在连续红色的结点。

  • 对于新插入的结点,若其父亲颜色为黑色,则满足红黑树的规则,无需调整。
  • 而若其父亲颜色为红色,则规则被破坏,需要调整。

🎄3.对于规则被破坏的情况,进行调整

在规则被破坏的前提下, 总共存在三种情况:1.只需变色;2.单旋+变色;3.双旋+变色。由于旋转操作在AVL树中已经详细介绍,若不清楚则可参考之前AVL树中关于旋转的内容:AVL树的旋转

  • 情况一:若parent,grandparent以及ubcle结点存在,其中p、u为红色,g为黑色,此时只需变色,将parent和uncle变为黑色,grandparent变为红色,由于g所在的树可能为一棵子树,故此时仍需向上调整。

  • 情况二:p存在为红色,g存在为黑色,u存在为黑色/u不存在,其中旋转为单旋情况。左边高->右单旋;若右边高->左单旋,旋转完后,p变为黑色,g变为红色

  • 情况三:p存在为红色,g存在为黑色,u存在为黑色/u不存在,其中cur为双旋情况。双旋之后,cur为黑色,parent和grandparent为红色。

🎄插入操作代码实现

综上所述,红黑树的代码实现如下:

	bool Insert(const pair<K,V>& kv)
	
		if (_root == nullptr)
		
			//_root = new Node(data);
			_root = new Node(kv);

			_root->_color = BLACK;
			return true;
		
		Node* cur = _root;
		Node* parent = _root;
		//找到要找到的位置
		while (cur)
		
			//KeyOfT kot;
			//if (kot(cur->_data) < kot(data))
			if(cur->_kv.first < kv.first)
			
				parent = cur;
				cur = cur->_right;
			
			//else if(kot(cur->_data) > kot(data))
			else if(cur->_kv.first > kv.first)
			
				parent = cur;
				cur = cur->_left;
			
			else//数据冗余,插入失败
			
				return false;
			
		
		//cur为要插入的位置
		//cur = new Node(data);
		cur = new Node(kv);
		cur->_color = RED;
		cur->_parent = parent;
		//Node* newnode = cur;//保存当前的cur位置
		//if (kot(cur->_data) < kot(data))
		if(parent->_kv.first < kv.first)
		
			parent->_right = cur;
		
		else
		
			parent->_left = cur;
		
		while (parent && parent->_color == RED)
		
			//由于parent存在且颜色为红,故panret一定不为根
			//那么grandfather一定存在
			Node* grandparent = parent->_parent;
			
			if (parent == grandparent->_left)
			
				Node* uncle = grandparent->_right;

				//parent为红色,则grandfather一定不是红色,否则破坏规则
				if (uncle && uncle->_color == RED)
				
					//情况一,uncle存在且为红
					cur->_color = RED;
					parent->_color = BLACK;
					uncle->_color = BLACK;
					grandparent->_color = RED;
					//此时仍需继续向上调整
					cur = grandparent;
					parent = cur->_parent;
				
				else
				
					if (cur == parent->_left)
					
						//情况二,uncle不存在或为黑
						//右单旋
						RotateR(grandparent);
						//更新颜色
						parent->_color = BLACK;
						grandparent->_color = RED;
					
					else//情况三
					
						//左右双旋
						RotateL(parent);
						RotateR(grandparent);
						cur->_color = BLACK;
						grandparent->_color = RED;
					
					//调整后符合红黑树规则,跳出循环
					break;
				
			
			else
			
				Node* uncle = grandparent->_left;
				if (uncle && uncle->_color == RED)
				
					//情况一,uncle存在且为红
					cur->_color = RED;
					parent->_color = BLACK;
					uncle->_color = BLACK;
					grandparent->_color = RED;
					//此时仍需继续向上调整
					cur = grandparent;
					parent = cur->_parent;
				
				else
				
					if (cur == parent->_right)//情况二
					
						//左单旋
						RotateL(grandparent);
						//更新颜色
						parent->_color = BLACK;
						grandparent->_color = RED;
					
					else//情况三
					
						//右左双旋
						RotateR(parent);
						RotateL(grandparent);
						cur->_color = BLACK;
						grandparent->_color = RED;
					
					//调整后符合红黑树规则,跳出循环
					break;
				
			
		
		_root->_color = BLACK;//不论如何,直接将根节点颜色置为黑色
		return true;
	

💡验证是否为红黑树

⏰1.根节点为黑色

  • 首先判断,根节点的颜色,若根节点为红色,破坏规则二,返回false。
	bool IsRBTree()
	
		if (_root == nullptr)//空树也是红黑树
			return true;
		//检测性质二:根节点是黑色
		if (_root->_color != BLACK)
		
			cout << "违反规则二,根结点为红色" << endl;
			return false;
		
		//判断性质三、四
		//...
	

⏰2.不存在连续的两个红色结点

  • 其次,对于规则三,若存在连续的两个红色结点,说明规则三被破坏了,返回false。
    这里需要注意的是,若去判断红色结点的孩子结点是否为红色,则还需要判断孩子是否存在;因此改为判断红色结点的父亲是否为红色,这样就可以简化代码及操作。
bool Check_RED_RED(Node* root)
	
		//由于在调用函数中已经检测了根节点的合法性
		//故此处的root结点必不为红色
		if (root == nullptr)
			return true;
		if (root->_color == RED && root->_parent->_color == RED)
		
			cout << "违反规则三,有连续的红色结点" << endl;
			return false;
		
		return Check_RED_RED(root->_left) && Check_RED_RED(root->_right);
	

⏰3.每条路径的黑色结点数量都相等

判断规则四的思路为先计算一条路径的黑色结点数量,然后遍历其他各条路径,对比黑色结点的数量,若不相等,则返回false。

//benchMark:基准值
	bool Check_BlackNum(Node* root, int benchMark, int blacknum)
	
		if (root == nullptr)//到空结点,此时blacknum为该条路径的黑色结点数量
		
			if (blacknum == benchMark)
				return true;
			else
				return false;
		
		if (root->_color == BLACK)
			blacknum++;
		return Check_BlackNum(root->_left, benchMark, blacknum);
	

⏰验证红黑树代码实现

	bool IsRBTree()
	
		if (_root == nullptr)//空树也是红黑树
			return true;
		//检测性质二:根节点是黑色
		if (_root->_color != BLACK)
		
			cout << "违反规则二,根结点为红色" << endl;
			return false;
		
		//计算最左路径上黑色结点的数量作为基准值
		int benchMark = 0;
		Node* cur = _root;
		while (cur)
		
			if (cur->_color == BLACK)
				benchMark++;
			cur = cur->_left;
		
		int blacknum = 0;
		return Check_RED_RED(_root) && Check_BlackNum(_root, benchMark, blacknum);
	

💡用红黑树封装实现map和set

在介绍完红黑树后,我们立刻来封装实现map和set。由此,接下来我们来实现红黑树的迭代器。

🌕红黑树的迭代器

红黑树作为关联式容器,其迭代器与list类似,也是封装一个指针,来保证各结点的访问。

template <class T, class Ref, class Ptr>//自身,引用(实现*重载),指针(实现->重载),便于范围for
struct RBTreeIterator

	typedef RBTreeIterator<T, Ref, Ptr> Self;
	typedef RBTreeNode<T> Node;

	Node* _node;
	RBTreeIterator(Node* node = nullptr)
		:_node(node)
	
	//实现其他功能
private:
;
  • 重载*和->
	//让迭代器具有类似指针的功能
	Ref operator*()
	
		return _node->_data;
	
	Ptr operator->()
	
		return &_node->_data;
	
  • 迭代器的移动
	//前置++
	Self& operator++()
	
		Increment();
		return *this;
	
	//后置++
	Self operator++(int)
	
		Self tmp = *this;
		Increment();
		return tmp;
	
	//前置--
	Self& operator--()
	
		Decrement();
		return *this;
	
	//后置--
	Self operator--(int)
	
		Self tmp = this;
		Decrement();
		return *this;
	
private:
	void Decrement()//迭代器以中序向前走一步
	
		//1.若结点的左子树存在,则找左子树中最右下的结点
		if (_node->_left)
		
			_node = _node->_left;
			while (_node->_right)
				_node = _node->_right;
		
		else
		
			//2.若结点的左孩子不存在,则向上寻找直到其不为父亲的左孩子
			Node* parent = _node->_parent;
			while (parent && parent->_left == _node)
			
				_node = parent;
				parent = _node->_parent;
			
			_node = parent;
		
	
	void Increment()//迭代器以中序向后走一步
	
		//1.若结点的右子树存在,则找右子树中最左下的结点
		if (_node->_right)
		
			_node = _node->_right;
			while (_node->_left)
			
				_node = _node->_left;
			
		
		else//2.若结点的右子树不存在,则找到结点不是其父亲右孩子的结点
		
			Node* parent = _node->_parent;
			while (parent && parent->_right == _node)
			
				_node = parent;
				parent = _node->_parent;
			
			_node = parent;
		
	
  • 迭代器的比较
	bool operator==(const Self& s) const
	
		return _node == s._node;
	
	bool operator!=(const Self& s)const
	
		return _node != s._node;
	

🌕改造红黑树

这里有个问题,如何用一个红黑树实现Key模型的set及Key-Value模型的map。
因此,在红黑树中,我们将模板参数修改为:

template <class K, class T, class KeyOfT>

其中,K为key值,T对于set实例化来说是key值,而对于map实例化来说则是pair<K,V>键值对。KeyOfT(从T中提取key值),此参数是为了便于插入时的比较,因为插入的参数修改为:bool Insert(T& data)

	iterator begin()
	
		//中序遍历的第一个结点		
		return iterator(LeftMost());
	
	iterator end()
	
		return iterator(nullptr);
	
	Node* LeftMost()//中序遍历的第一个结点
	
		Node* left = _root->_left;
		while (left && left->_left)
			left = left->_left;
		return left;
	
	Node* RightMost()中序遍历的最后一个结点
	
		Node* right = _root->_right;
		while (right && right->_right)
			right = right->_right;
		return right;
	

🌕set的封装实现

map内部直接封装了红黑树,实现key模型的二叉搜索树,所有接口直接调用红黑树的接口即可:

	template <class K>
	class Set
	
	public:
		struct SetKeyofT
		
			const K& operator()(const K& key)//对set而言,T为key,直接返回key值进行插入比较即可
			
				return key;
			
		;
		typedef typename RBTree<K, K, SetKeyofT>::iterator iterator;

		iterator begin()
		
			return _t.begin();
		

		iterator end()
		
			return _t.end();
		

		pair<iterator, bool> insert(const K& key)
		
			return _t.Insert(key);
		

	private:
		//Set内部封装红黑树,K和T参数都传入key值即可
		//Set内部再定义SetKeyofT结构体,里面重载了()操作符
		//返回Set对应的key值
		RBTree<K, K, SetKeyofT> _t;
	;

🌕map的封装实现

map 的封装实现与set并无太大的区别,只不过map中重载了[]下标访问限定符:

	class Map
	
	public:
		struct MapKeyofT
		
			const K& operator()(const pair<const K, V>& data)
			
				return data.first;
			
		;
		typedef typename RBTree<K, pair<const K, V>, MapKeyofT>::iterator iterator;
		iterator begin()
		
			return _t.begin();
		
		iterator end()
		
			return _t.end();
		
		pair<iterator, bool> insert(const pair<

以上是关于C++红黑树的主要内容,如果未能解决你的问题,请参考以下文章

初识C++之红黑树

算法导论 之 红黑树 - 删除[C语言]

红黑树 实现

C++ 红黑树

C++红黑树

C++红黑树