【C++】红黑树


个人主页:🍝在肯德基吃麻辣烫

我的gitee:C++仓库

个人专栏:C++专栏


文章目录


前言

本文着重讲解红黑树的原理和性质及其难点。


以下是本篇文章正文内容

一、什么是红黑树?

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。

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

也就是说,红黑树任意一条节点的路径长度都不超过最短路径的2倍。

二、红黑树的性质

    1. 每个结点不是红色就是黑色
    1. 根节点是黑色的
    1. 如果一个节点是红色的,则它的两个孩子结点是黑色的(反过来说,不能有连续的红色节点出现)
    1. 每一条路径的黑色节点数量均相同
    1. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)

为什么满足了上述性质,这棵红黑树就满足最长路径不超过最短路径的2倍?

在极限情况下,最短路径是全黑的路径 ,最长路径是一黑一红相间的路径,其余的每条路径的长度就是介于这两个节点的路径长度之间。所以只要符合上述性质,一定能满足最长路径不超过最短路径的2倍。

三、红黑书节点的定义

cpp 复制代码
enum Colour
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
public:
	RBTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED)
	{}

	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	pair<K, V> _kv;
	Colour _col;

};

四、红黑树的插入操作

  • 1)找到待插入位置

由于红黑树也是一棵二叉搜索树,所以如果插入节点的值比根小,则向左走;如果插入节点的值比根大,则向右走。

注意这里有一些细节:

    • 1)新插入的节点一定是红色的
      • 如果新插入节点是黑色的,则会违反性质4,会影响到整棵树,影响到每一条路径黑色节点数量是否相等。
      • 如果新插入节点是红色的,假如插入节点的父亲是红色的,则违反性质3,在后续进行变色调整即可,如果插入节点的父亲是黑色的,则没有影响,直接就可以结束插入操作。
  • 2)插入完成后,判断还是否符合红黑树的性质

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

下面的情况是p在g的左的情况。

情况1:变色

  • 1.如果cur为红,p为红,u存在且为红,则需要进行变色。

  • 将p和u从红色变成黑色,再让g由黑色变成红色。

注意,这里有一个细节,这棵树有可能只是一棵局部的树,也就是说,g可能还有父亲。

如果g的父亲是红色,则让cur走到g的位置,p走到g的父亲的位置,继续向上调整。

如果g的父亲是黑色,则结束了,退出。

如果g没有父亲(g是根节点),让g再变回黑色即可。

情况2:旋转+变色

  • 2.如果cur为红,p为红,u不存在/u存在且为黑

下面是u存在且为黑的情况

2.1) 如果cur是p的左孩子,此时的情况就是单纯的左边高,对g进行右旋,最后变色即可。

具体的旋转过程参考上一篇文章AVL树的旋转,所有旋转细节都已经讲清楚了。

变色前:
cur是红的,p是红的,g是黑的,u是黑的

变色后:
cur是红的,p是黑的,g是红的,u是黑的

注意这里的细节:如果旋转前g有父亲,则旋转后需要让p指向g之前的父亲。

下面是u不存在的情况:

u不存在就更简单了,操作过程同上。

如果cur是p的右孩子,此时的情况就是折线形,也就是下面的情况:

此时需要进行左右双旋。

1.对p进行左旋后,再对g进行右旋,最后变色即可。

变色前:cur是红,p是红,g是黑。

变色后:cur是黑,p是红,g是红。

u不存在就更简单了,操作过程同上。

注意这里的细节:如果旋转前g有父亲,则旋转后需要让p指向g之前的父亲。

如果是p在g的右的情况

每种情况都是一样的,只不过是方向变了,照葫芦画瓢即可。

总结:

重点是看u(叔叔)节点

如果p是g的左的情况:

  • 情况1:变色

    • 1)如果cur为红,p为红,u存在且为红,则需要进行变色。
  • 情况2:旋转+变色

    • 1)如果cur为红,p为红,u不存在/u存在且为黑
      • 如果cur是p的左,则为单纯的左边高,对g进行右旋后,再进行变色即可。
      • 如果cur是p的右,则为折线形,先对p进行左旋,再对g进行右旋,最后进行变色即可。

红黑树插入节点代码

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
	}

	Node* cur = _root;
	Node* cur_parent = _root;

	//1.找到待插入位置
	while (cur)
	{
		if (cur->_kv.first < kv.first)
		{
			cur_parent = cur;
			cur = cur->_right;
		}
		else if (cur->_kv.first > kv.first)
		{
			cur_parent = cur;
			cur = cur->_left;
		}
		else
			return false;
	}

	//2.先判断待插入节点是在parent的左边还是右边
	cur = new Node(kv);
	//新插入的节点必须是红的
	cur->_col = RED;
	if (cur_parent->_kv.first > kv.first)
	{
		cur_parent->_left = cur;
	}
	else
	{
		cur_parent->_right = cur;
	}
	//连接parent
	cur->_parent = cur_parent;

	//开始判断是否还符合红黑树的性质

	while (cur_parent && cur_parent ->_col == RED)
	{
		Node* grandfather = cur_parent->_parent;
		//左边的情况
		if (grandfather->_left == cur_parent)
		{
			Node* uncle = grandfather->_right;

			//u存在且为红
			if (uncle && uncle->_col == RED)
			{
				cur_parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
				Node* ppNode = grandfather->_parent;
				if (ppNode == nullptr)
				{
					grandfather->_col = BLACK;
					//g是根,变色后可以break了
					break;
				}
				//向上调整
				else if (ppNode->_col == RED)
				{
					cur = grandfather;
					cur_parent = ppNode;
				}
				else
				{
					//ppNode是黑色,已经完成了,可以break
					break;
				}
			}

			//u不存在/u存在且为黑
			else 
			{
				//单纯左边高,右单旋
				if (cur == cur_parent->_left)
				{
					RotateR(grandfather);
					//变色
					cur_parent->_col = BLACK;
					grandfather->_col = RED;
				}
				//折线形
				//    g
				//  p
				//    c
				else
				{
					RotateL(cur_parent);
					RotateR(grandfather);
					//变色
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				//旋转+变色后,一定平衡了,可以break了
				break;
			}
			
		}

		//右边的情况
		//   g                g
		//     p      或         p
		//       c            c
		else
		{
			Node* uncle = grandfather->_left;

			//u存在且为红
			if (uncle && uncle->_col == RED)
			{
				cur_parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;
				Node* ppNode = grandfather->_parent;
				//1.ppNode为空
				if (ppNode == nullptr)
				{
					grandfather->_col = BLACK;
					//g是根,变色后可以break了
					break;
				}
				//2.ppNode存在且为红
				//向上调整
				else if (ppNode->_col == RED)
				{
					cur = grandfather;
					cur_parent = ppNode;
				}
				
				else
				{
					//ppNode是黑色,已经完成了,可以break
					break;
				}
			}

			//u不存在/u存在且为黑
			//   g                g
			//     p      或         p
			//       c            c
			else
			{
				//单纯右边高,左单旋
				if (cur == cur_parent->_right)
				{
					RotateL(grandfather);
					//变色
					cur_parent->_col = BLACK;
					grandfather->_col = RED;
				}

				//折线形
				//    g
				//      p
				//    c
				else
				{
					RotateR(cur_parent);
					RotateL(grandfather);
					//变色
					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				//旋转+变色后,一定平衡了,可以break了
				break;
			}
		}

	}
	//不管发生什么样的情况,这个代码一定没有错
	_root->_col = BLACK;
	return true;
}


//左单旋
void RotateL(Node* parent)
{
	++CountRotate;
	Node* cur = parent->_right;
	Node* curleft = cur->_left; //这个是cur的左子树,旋转后变成了parent的右孩子
	Node* ppNode = parent->_parent;

	parent->_right = curleft;
	if (curleft)
		curleft->_parent = parent;

	cur->_left = parent;
	parent->_parent = cur;

	if (!ppNode)
	{
		_root = cur;
		cur->_parent = nullptr;
	}
	else
	{
		if (ppNode->_right == parent)
		{
			ppNode->_right = cur;
		}
		else
		{
			ppNode->_left = cur;
		}

		cur->_parent = ppNode;
	}

}

//右单旋
void RotateR(Node* parent)
{
	++CountRotate;
	Node* cur = parent->_left;
	Node* curright = cur->_right;
	Node* ppNode = parent->_parent;

	parent->_left = curright;
	//如果cur的右子树是空
	if (curright)
		curright->_parent = parent;

	cur->_right = parent;
	parent->_parent = cur;

	if (!ppNode)
	{
		_root = cur;
		cur->_parent = nullptr;
	}
	else
	{
		cur->_parent = ppNode;
		//要知道根的左边是cur还是右边是cur
		if (ppNode->_left == parent)
		{
			ppNode->_left = cur;
		}
		else
		{
			ppNode->_right = cur;
		}
	}

}

void RotateLR(Node* parent)
{
	Node* cur = parent->_left;
	Node* curright = cur->_right;

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

}

void RotateRL(Node* parent)
{
	Node* cur = parent->_right;
	Node* curleft = cur->_left;

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

}

五、验证一棵树是否为红黑树

  • 1)验证中序遍历是否是有序序列
  • 2)验证红黑树的各个性质
cpp 复制代码
	int Height()
	{
		return _Height(_root);
	}


	bool IsRBTree()
	{
		cout << "IsRBTree():";
		return _IsRBTree(_root);
	}

	//判断一棵树是否为红黑树
	//1.判断他的中序遍历
	//2.判断红黑树的每一个性质


	bool _IsRBTree(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}
		
		//性质1,每个节点不是红就是黑,不用判断
		//性质2,根为黑
		if (root->_col == RED)
		{
			cout << "根不是黑色的,不是红黑树" << endl;
			return false;
		}

		//检测3和4
		//性质3
		//不能出现连续的红节点
		//性质4
		//每条路径上的黑色节点个数相同
		int benchmark = 0;
		Node* benchNode = root;
		//基准路径选最左边
		while (benchNode)
		{
			if (benchNode->_col == BLACK)
				++benchmark;
			benchNode = benchNode->_left;
		}

		if (!CheckColour(root,0,benchmark))
		{
			return false;
		}
		return true;
	}

	//检验性质3,检验自己和父亲的颜色比检验自己和左右孩子的颜色更方便
	//检验性质4,先随便求一条路径的黑色节点,然后再求其他路径的节点
	//跟这条基准路径对比,不匹配就不符合性质4
	//就算基准路径求出来是错的,其他路径是对的,如果两者对不上,那就不是红黑树
	bool CheckColour(Node* root,int blacknum,int benchmark)
	{
		if (root == nullptr)
		{
			if (blacknum != benchmark)
				return false;
			
			return true;
		}

		if (root->_col == BLACK)
			++blacknum;
		
		if (root->_col == RED && root->_parent && root->_parent->_col == RED)
		{
			cout << "出现了连续的红节点,不是红黑树" << endl;
			return false;
		}

		return CheckColour(root->_left,blacknum,benchmark) && CheckColour(root->_right, blacknum, benchmark);
	}



	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int left = _Height(root->_left);
		int right = _Height(root->_right);
		return left > right ? left + 1 : right + 1;
	}

六、比较AVL树和红黑树

红黑树 AVL树
时间复杂度 O(logN) O(logN)
树有10亿个值时 查找2*logN->60次 查找logN->30次

可见,红黑树和AVL树的查找次数是在同一个量级的,但由于AVL树要保持绝对平衡,所以需要更频繁地旋转操作。

当我用一千万个随机数进行测试时,结果如下:

AVL树的旋转次数在400多万次左右,红黑树的旋转次数在300多万,相差了几十万次,这里就体现出了差距。

不过,如果插入的数据是有序的,则AVL树的效率相对更高一点:

插入的数越随机,红黑树的效率越占优势。

所以实际上,红黑树的效率是比AVL树的效率要高一些的。实际中更多应用的也是红黑树


总结

这篇文章主要讲述对红黑树的插入操作,在插入节点后需要保证红黑树的性质,所以引出了变色,旋转+变色等操作。

相关推荐
java1234_小锋40 分钟前
MyBatis如何处理延迟加载?
java·开发语言
FeboReigns1 小时前
C++简明教程(4)(Hello World)
c语言·c++
FeboReigns1 小时前
C++简明教程(10)(初识类)
c语言·开发语言·c++
学前端的小朱1 小时前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
zh路西法1 小时前
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
c++·游戏·unity·设计模式·状态模式
.Vcoistnt2 小时前
Codeforces Round 994 (Div. 2)(A-D)
数据结构·c++·算法·贪心算法·动态规划
小k_不小2 小时前
C++面试八股文:指针与引用的区别
c++·面试
摇光932 小时前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
沐泽Mu2 小时前
嵌入式学习-QT-Day07
c++·qt·学习·命令模式
沐泽Mu2 小时前
嵌入式学习-QT-Day09
开发语言·qt·学习