【C++】红黑树

目录

红黑树简介

为了保持AVL树的平衡性,在插入和删除操作后,会非常频繁地调整全树的整体拓扑结构,代价比较大,为此在AVL树的基础上放宽一些条件,则引入了红黑树结构。

红黑树是一棵二叉搜索树 ,它和AVL树类似,也需要通过旋转来调整树的高度,避免在极端情况下造成单边树。但它的旋转又不像AVL树那么严格,它是通过颜色来约束的

红黑树中,它的每个节点都增加一个存储位来表示节点颜色,可以是红色也可以是黑色。有了颜色,以及旋转的条件,使得红黑树也差不多能达到平衡的效果,相比AVL树不会太差。

即:
AVL树------严格平衡
红黑树------近似平衡

红黑树的性质

红黑树的性质有以下几点:

  1. 每个节点不是黑色就是红色
  2. 根节点是黑色的
  3. 一条路径中,没有连续的红色节点
  4. 每条路径黑色节点数量相等
  5. 叶子节点(即虚构的外部节点、NULL节点)都是黑色的。

满足以上性质,红黑树就能保证:最长路径中节点个数不会超过最短路径节点数的二倍 。即最短路径*2≥最长路径

王道上有一句小口诀:左根右,根叶黑,不红红,黑路同

下面就是一棵标准的红黑树。

复制代码
下面解答一下为什么红黑树能保证:最长路径中节点个数不会超过最短路径节点数的二倍。
红黑树中最短路径:该路径中是全黑,个数设其为n
红黑树中最长路径:一红一黑相间排列,则红节点数≤黑节点数,则个数最多为2n。
所以则有:最短路径*2≥最长路径。

注意:在数路径时,不能数有多少个叶子节点作为路径,而应该以空节点为终点来数。

则使用红黑树来查找时,查找最短路径上的时间复杂度是logn,而对于最长路径上查找时是2logn。但就算n非常大,就算是一亿,此时也只需要最长查找60次,对cpu来说,这些次数不算什么,因此在查找上红黑树和AVL树是差不多的。

但在插入和删除时红黑树更具有优势

插入节点的探讨

对于红黑树,我们就要考量插入的时候到底插入红色节点还是黑色节点。

首先给出结论:默认插入红色节点。

有连续的红节点就有可能最长路径超过最短路径的二倍。但此时我们可以通过改变颜色和旋转来解决。而如果增加的黑色,无论怎么旋转变颜色,就不能保证每条路径黑色节点数量都相同。

红黑树的插入

红黑树从头到尾只看是否满足它所需的性质,并没有看它的高度。

红黑树本来就不想过多的旋转,它会先通过变颜色来调整。

首先红黑树的插入可定为以下规则:

  1. 先查找,确定插入位置(类似AVL树),插入新节点
  2. 新节点为根------则染为黑色
  3. 新节点为非根------则染为红色

红黑树如何调整,则需要看叔叔的脸色,也就是父亲节点的兄弟节点的颜色。

对于红黑树的调整和AVL树的调整差不多,AVL树是看平衡因子,哪个不符合规则就调整,并一直往上调整。红黑树也是一样,主要是看新增节点,父亲节点,叔叔节点,爷爷节点之间的颜色关系 ,然后来进行调整,并要连续向上调整。

只需要我每次调整它都是红黑树的状态,我就能保证它到最后一定是一棵红黑树,有点类似递归。

红黑树插入节点会保证每条路径的黑色节点数相同,若出现两个连续的红节点,则会进行调色或旋转来处理。


此时我们可以大致分为以下两种情况:

  1. 新增节点为红,父亲节点为红,爷爷节点为黑,叔叔节点存在且为红

父亲红,叔叔红

此时是最简单的一种,只需要调整颜色即可:

只需要把父亲和叔叔都变为黑色,把爷爷变为红色,此时再把爷爷节点当做新插入的节点,继续向上调整

  1. 新增节点为红,父亲节点为红,爷爷节点为黑,叔叔节点不存在或存在且为黑

父亲红,叔叔黑,此时则要考虑旋转。

类似于AVL树,同样有左单旋,右单旋,左右双旋,右左双旋这四种,只是还会加上颜色变换。


叔叔都是黑色或不存在,此时的旋转规则如下:

  • LL型:右单旋,父换爷+染色

  • RR型:左单旋,父换爷+染色

  • LR型:左右双旋,儿(cur)换爷+染色

  • RL型:右左双旋,儿(cur)换爷+染色

下面是插入的完整代码:

cpp 复制代码
bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		Node* k = new Node(kv);
		_root = k;
		_root->_col = BLACK;//根节点为黑节点
		return true;
	}

	//找到要插入的位置
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (cur->_kv.first > kv.first)//往左走
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_kv.first < kv.first)//往右走
		{
			parent = cur;
			cur = cur->_right;
		}
		else
			return false;//找到重复的,直接退出,不允许插入一样的值
	}

	//找到要插入的位置了
	cur = new Node(kv);

	//新增节点的颜色定为红色
	cur->_col = RED;
	cur->_parent = parent;
	if (parent->_kv.first > kv.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}

	while (parent && parent->_col == RED)//父亲节点为红,则一定有爷爷节点,因为根节点为黑
	{
		Node* grandfather = parent->_parent;
		if (parent == grandfather->_left)
		{
			Node* uncle = grandfather->_right;
			//uncle存在且uncle为红色------>单纯变色再继续往上处理
			if (uncle && uncle->_col == RED)
			{
				uncle->_col = parent->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}

			//uncle存在且为黑,或者不存在------>旋转+变色,再继续向上处理
			else
			{
				
				if (cur == parent->_left)//右单旋,父和爷染色
				{
					//     g
					//  f     u
					//c
					RotateR(grandfather);

					parent->_col = BLACK;
					grandfather->_col = RED;

				}
				else//双旋,左右双旋,儿爷染色
				{
					//         grandfather
					//  father              uncle
					//            cur
					RotateL(parent);
					RotateR(grandfather);

					cur->_col = BLACK;
					grandfather->_col = RED;
				}

				//当旋转变色完成之后,已达到平衡,可直接跳出
				break;
			}
		}
		else//parent == grandfather->_right
		{
			Node* uncle = grandfather->_left;
			//uncle存在且uncle为红色------>单纯变色再继续往上处理
			if (uncle && uncle->_col == RED)
			{
				uncle->_col = parent->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}

			//uncle存在且为黑,或者不存在------>旋转+变色,再继续向上处理
			else
			{

				if (cur == parent->_right)//左单旋,父和爷染色
				{
					//     g
					//  u     p
					//          c
					RotateL(grandfather);

					parent->_col = BLACK;
					grandfather->_col = RED;

				}
				else//双旋,右左双旋,儿爷染色
				{
					//         grandfather
					//  uncle               parent
					//            cur
					RotateR(parent);
					RotateL(grandfather);

					cur->_col = BLACK;
					grandfather->_col = RED;
				}

				//当旋转变色完成之后,已达到平衡,可直接跳出
				break;
			}
		}
	}

	_root->_col = BLACK;//简单粗暴的把根变为黑
	return true;
}

下面是往一棵空的红黑树依次插入节点的过程图:

从一棵空的红黑树开始依次插入:20,10,5,30,40


判断是否是红黑树

  1. 首先空树一定是红黑树,如果根不是黑色,则直接断定不是红黑树
  2. 要看是否有连续的红节点
  3. 看每条路径上的黑色节点数量是否相同

对于是否有连续的红节点,只需要看当前节点和它的父节点的颜色是否都是红色的即可。

对于每条路径上的黑色节点数量是否相同,比较难想到:

  • 我们先算出最左侧路径的黑色节点数量,将其作为参考值。
  • 给每个节点设置一个值,用来记录从根节点到该节点一共有多少个黑色节点。
  • 调用递归,当递归到null节点时,判断黑色节点数和参考值是否相同,如果相同则正确。
    代码如下:
cpp 复制代码
public:
	bool IsBalance()
	{
		if (_root == nullptr)
			return true;

		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;
		}

		if (root->_col == BLACK)
		{
			blackNum++;
		}

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

感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。本篇还涵盖了王道的内容与图片。

相关推荐
天若有情67312 小时前
程序员原创|借鉴JS事件冒泡,根治电脑文件混乱的“冒泡整理法”
开发语言·javascript·windows·ecmascript·电脑·办公·日常
墨染千千秋12 小时前
C++函数的使用以及主函数
c++
特种加菲猫12 小时前
继承,一场跨越时空的对话
开发语言·c++
WBluuue13 小时前
Codeforces 1093 Div2(ABCD1D2)
c++·算法
玩转单片机与嵌入式13 小时前
玩转边缘AI(TInyML):需要掌握的C++知识汇总!
开发语言·c++·人工智能
历程里程碑14 小时前
4 Git远程协作:从零开始,玩转仓库关联与代码同步(带实操代码讲解)
大数据·c++·git·elasticsearch·搜索引擎·gitee·github
茉莉玫瑰花茶14 小时前
Qt 信号与槽 [ 1 ]
开发语言·数据库·qt
汉克老师14 小时前
GESP5级C++考试语法知识(贪心算法(一)课堂例题精讲)
c++·贪心算法·gesp5级·gesp五级·贪心规律
墨染千千秋14 小时前
C++头文件的使用,和各个头文件与头文件用处
c++