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

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

相关推荐
lclin_20202 小时前
【大恒相机】C++ 设备枚举+打开关闭+启停采集(基础入门)
c++·工业相机·大恒相机·galaxysdk
雒珣2 小时前
Qt实现命令行参数功能示例:QCommandLineParser
开发语言·数据库·qt
无巧不成书02182 小时前
Java异常体系与处理全解:核心原理、实战用法、避坑指南
java·开发语言·异常处理·java异常处理体系
大尚来也2 小时前
Go性能调优实战:用pprof精准定位瓶颈
开发语言
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 131. 分割回文串 | C++ 回溯算法基础切割法
c++·算法·leetcode
User_芊芊君子2 小时前
2026 Python+AI入门|0基础速通,吃透热门轻量化玩法
开发语言·人工智能·python
aq55356002 小时前
Laravel7.x重磅升级:十大新特性解析
开发语言·汇编·c#·html
大鹏说大话2 小时前
Go语言Channel并发编程实战:从基础通信到高级模式
开发语言·后端·golang
Jacky-0082 小时前
Rust安装(MinGw64编译器安装)
开发语言·后端·rust