当前位置: 首页 > article >正文

第23课-C++-红黑树的插入与旋转

🌇前言

红黑树是一种自平衡的二叉搜索树,因其出色的性能,广泛应用于实际中。Linux 内核中的 CFS 调度器便是一个使用红黑树的例子,这足以说明它的重要性。红黑树的实现通过红黑两种颜色的控制来维持平衡,并在必要时使用旋转操作来降低树的高度,使之保持平衡。

🏙️正文

1. 认识红黑树

红黑树最初由德国慕尼黑大学的 Rudolf Bayer 教授于1978年发明,后来由 Leo J. Guibas 和 Robert Sedgewick 修改为我们今天所熟悉的红黑树。红黑树在二叉搜索树的基础上,增加了颜色属性,通过一些规则降低树的高度。

与 AVL 树的严格平衡策略不同,红黑树仅在需要时进行旋转,从而减少了旋转操作的开销。虽然 AVL 树在极端情况下可能比红黑树快一倍,但红黑树在插入、删除、修改操作中性能更优。

1.1、红黑树的定义

红黑树 也是 三叉链 结构,不过它没有 平衡因子,取而代之的是 颜色

红黑树节点的定义

// 节点的颜色
enum Color{RED, BLACK};
// 红黑树节点的定义
template<class ValueType>
struct RBTreeNode
{
RBTreeNode(const ValueType& data = ValueType(),Color color = RED)
: _pLeft(nullptr), _pRight(nullptr), _pParent(nullptr)
, _data(data), _color(color)
{}
RBTreeNode<ValueType>* _pLeft; // 节点的左孩子
RBTreeNode<ValueType>* _pRight; // 节点的右孩子
RBTreeNode<ValueType>* _pParent; // 节点的双亲(红黑树需要旋转,为了实现简单给
出该字段)
ValueType _data; // 节点的值域
Color _color; // 节点的颜色
}

注意: 定义新节点时,颜色可以为也可以为 黑,推荐为 色,具体原因后面解释。

1.2 红黑树的性质

红黑树有以下五条性质:

  1. 每个节点不是红色就是黑色。
  2. 根节点是黑色的。
  3. 如果一个节点是红色,那么它的两个子节点必须是黑色(不能有连续的红节点)。
  4. 从任一节点到其所有后代的叶子节点的路径中,都包含相同数量的黑色节点。
  5. 每个叶子节点都是黑色的空节点。

1.3 红黑树的特点

红黑树在插入节点时,默认将新节点颜色设为红色。这种设计简化了调整,因为红色新节点仅在特定条件下会触发旋转调整。

红黑树的路径特点如下:

  • 最长路径:红黑相间
  • 最短路径:全是黑节点

红黑树在极端情况下最长路径为最短路径的两倍,但这种情况很少见。因此在实际应用中,红黑树的查询性能与 AVL 树差异不大,而在涉及旋转的操作中,红黑树优势明显。

2. 红黑树的插入操作

2.1、抽象图

在演示 红黑树 的插入操作时,也需要借助 抽象图,此时的 抽象图 不再代表高度,而是代表 黑色节点 的数量。

 2.2、插入流程

红黑树 的插入流程也和 二叉搜索树 基本一致,先找到合适的位置,然后插入新节点,当节点插入后,需要对颜色进行判断,看看是否需要进行调整

插入流程: 判断根是否为空,如果为空,则进行第一次插入,成功后返回 true 找到合适的位置进行插入,如果待插入的值比当前节点值大,则往 右 路走,如果比当前节点值小,则往 左 路走 判断父节点与新节点的大小关系,根据情况判断链接至 左边 还是 右边 根据颜色,判断是否需要进行 染色、旋转 调整高度 整体流程如下(不包括染色调整的具体实现)

bool Insert(const std::pair<K, V> kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;	//根节点一定是黑色
		return true;
	}

	//寻找合适位置
	Node* parent = nullptr;
	Node* cur = _root;
	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;
		}
	}

	//插入节点
	cur = new Node(kv);
	if (parent->_kv.first < kv.first)
		parent->_right = cur;
	else
		parent->_left = cur;
	cur->_parent = parent;

	//判断是否需要 染色、旋转
	while (parent && parent->_col == RED)
	{
		Node* grandfather = parent->_parent;	//祖父节点
		//……
	}
	
	return true;
}

红黑树 如何调整取决于 叔叔,即父亲的兄弟节点 。并且如果父亲为黑,直接插入就行了,不必调整 如果父亲为红并且叔叔也为红,可以只通过染色解决当前问题,然后向上走,继续判断是否需要调整,如果父亲为红并且叔叔为黑或者叔叔不存在,此时需要 旋转 + 染色,根据当前节点与父亲的位置关系,选择 单旋 或 双旋,值得一提的是旋转 + 染色 后,不必再向上判断,可以直接结束调整 关于旋转的具体实现,这里不再展开叙述,可以复用 AVL 中的旋转代码,并且最后不需要调整平衡因子 《C++【AVL树】》 注意:红黑树的调整可以分为 右半区 和 左半区 两个方向(根据 grandfather 与 parent 的位置关系而定),每个方向中都包含三种情况:单纯染色、单旋+染色、双旋+染色,逐一讲解费时费力,并且两个大方向的代码重复度极高,因此 下面的旋转操作基于 右半区 左半区 的操作和 右半区 基本没啥区别,可以去完整代码中求证。

2.3、单纯染色

如果父亲为黑色,则不需要调整,不讨论这种情况,下面三种情况基本要求都是:父亲为红

当新节点插入后,如果 叔叔 节点也为 红色,那么可以通过将 祖父 节点的黑色素下放给 父亲和叔叔,祖父节点 变为 红色,这样调整仍可确保 每条路径中的黑色节点数目相同

单次染色还不够,需要从 grandfather 处继续向上判断是否需要 调整,单纯染色后,向上判断可能会变成其他情况,这是不确定的,具体情况具体分析

单纯染色 的操作如下:

注意:c 表示当前节点,p 表示父亲节点,u 表示叔叔节点,g 表示祖父节点

修正: 动图中语句修正为 “父亲为红,叔叔也为红,直接染色即可” 当 单次染色 结束后,更新 cur 至 grandfather 的位置,并同步更新 parent,继续判断是需要进行 单纯染色、单旋 + 染色 还是 双旋 + 染色 本质:将公共的黑色下放给两个孩子。

代码:

//在右半区操作
Node* uncle = grandfather->_left;	//叔叔节点

if (uncle && uncle->_col == RED)
{
	//染色、向上更新即可
	grandfather->_col = RED;
	parent->_col = uncle->_col = BLACK;
	cur = grandfather;
	parent = cur->_parent;
}
else
{
	//此时需要 旋转 + 染色
	//……
}

叔叔 存在且为  很好处理,难搞的是 叔叔 不存在或 叔叔 为 ,需要借助 旋转 降低高度

注意: 此时的五个抽象图,都代表同一个具象图;如果 parent 为空,证明 cur 为根节点,此时需要把根节点置为 黑色,在返回 true 前统一设置即可。

2.4、左单旋 + 染色

单旋:右右、左左,此时在 右半区,所以当 叔叔 不存在或者为 黑色 且节点位于 父亲 的 右边 时,可以通过 左单旋 降低高度

如果在左半区,节点位于父亲的左边时,则使用 右单旋 降低高度

在高度降低后,需要使用 染色 确保符合 红黑树 的性质

旋转 思想很巧妙,在 旋转 + 染色 后,可以跳出循环,结束调整

左旋转 + 染色 的操作如下:

注意:c 表示当前节点,p 表示父亲节点,u 表示叔叔节点,g 表示祖父节点

显然,旋转 + 染色 后,parent 是一定会被修改为 黑色 的,所以不必再往上判断调整,因为现在已经很符合性质了(即使 parent 的父亲是 红色,也不会出现连续的 红色节点)

本质:将 parent 的左孩子托付给 grandfather 后,parent 往上提,并保证不违背性质。

//在右半区操作
Node* uncle = grandfather->_left;	//叔叔节点

if (uncle && uncle->_col == RED)
{
	//染色、向上更新即可
	//……
}
else
{
	//此时需要 旋转 + 染色
	if (parent->_right == cur)
	{
		//右右,左单旋 ---> parent 被提上去了
		RotateL(grandfather);
		grandfather->_col = RED;
		parent->_col = BLACK;
		cur->_col = RED;
	}
	else
	{
		//右左,右左双旋 ---> cur 被提上去了
		//……
	}

	//旋转后,保持平衡,可以结束调整
	break;
}

注意: 这种情况多半是由 单纯染色 转变而来的,所以不同区域的抽象图有不同的情况,必须确保能符合红黑树的性质。

2.5、右左双旋 + 染色

双旋:右左、左右,此时在 右半区,所以当 叔叔 不存在或者为 黑色 且节点位于 父亲 的 左边 时,可以通过 右左双旋 降低高度

如果在左半区,节点位于父亲的右边时,则使用 左右双旋 降低高度

在高度降低后,需要使用 染色 确保符合 红黑树 的性质

旋转 思想很巧妙,在 旋转 + 染色 后,可以跳出循环,结束调整

右左双旋 + 染色 的操作如下:

注意:c 表示当前节点,p 表示父亲节点,u 表示叔叔节点,g 表示祖父节点

双旋 其实就是两个不同的 单旋,不过对象不同而已,先 右旋转 parent,再 左旋转 grandfather 就是 右左双旋

本质:将 cur 的右孩子托付给 parent,左孩子托付给 grandfather 后,把 cur 往上提即可,并保证不违背 红黑树 的性质

红黑树的检验主要是为了确保在插入或删除节点后,红黑树的性质依旧得到了满足。我们可以通过以下几个方面来验证红黑树的合法性:


4.红黑树的性质回顾

为了检验红黑树的合法性,需要检查以下五条性质是否都被满足:

  1. 每个节点不是红色就是黑色
  2. 根节点是黑色
  3. 如果一个节点是红色,那么它的两个子节点必须是黑色(即不能有连续的红色节点)。
  4. 从任一节点到其所有后代叶子节点的路径上,必须包含相同数量的黑色节点
  5. 每个叶子节点的 nullptr 视为黑色

检验步骤

我们可以通过编写检查函数,递归地验证红黑树的合法性。

1. 验证根节点是否为黑色

红黑树的根节点必须是黑色的。如果根节点不是黑色,就说明红黑树不合法。这是一个简单的检查。

if (_root->_col != BLACK)
{
    std::cerr << "根节点不是黑色,违反红黑树性质二" << std::endl;
    return false;
}
2. 检查是否出现连续的红色节点

如果一个节点是红色,那么它的子节点必须是黑色。因此,当遍历树时,可以检查每个红色节点的子节点是否都是黑色。如果出现连续的红色节点,则树不合法。

bool CheckRedNodeRule(Node* node)
{
    if (node == nullptr)
        return true;

    if (node->_col == RED)
    {
        // 如果当前节点是红色,检查左右子节点
        if ((node->_left && node->_left->_col == RED) || 
            (node->_right && node->_right->_col == RED))
        {
            std::cerr << "出现连续的红色节点,违反红黑树性质三" << std::endl;
            return false;
        }
    }

    // 递归检查左右子树
    return CheckRedNodeRule(node->_left) && CheckRedNodeRule(node->_right);
}
3. 验证从根节点到每个叶子节点路径的黑色节点数量是否一致

每一条路径从根节点到叶子节点所包含的黑色节点数量必须一致。这个数量可以称为基准黑色节点数。

首先,从根节点到最左边叶子节点的路径中统计黑色节点数量作为基准值。然后,递归检查其他路径是否符合这个基准值。

// 计算左侧路径上的黑色节点数量作为基准
int GetBlackHeight(Node* node)
{
    int blackCount = 0;
    while (node != nullptr)
    {
        if (node->_col == BLACK)
            blackCount++;
        node = node->_left;
    }
    return blackCount;
}

// 检查每条路径的黑色节点数是否一致
bool CheckBlackHeight(Node* node, int blackCount, int benchMark)
{
    if (node == nullptr)
    {
        // 到达叶子节点,检查是否等于基准值
        return blackCount == benchMark;
    }

    if (node->_col == BLACK)
        blackCount++;

    return CheckBlackHeight(node->_left, blackCount, benchMark) &&
           CheckBlackHeight(node->_right, blackCount, benchMark);
}
4. 汇总检验

结合以上的检查点,我们可以编写一个 IsRBTree() 函数,逐步验证红黑树的合法性

// 计算左侧路径上的黑色节点数量作为基准
int GetBlackHeight(Node* node)
{
    int blackCount = 0;
    while (node != nullptr)
    {
        if (node->_col == BLACK)
            blackCount++;
        node = node->_left;
    }
    return blackCount;
}

// 检查每条路径的黑色节点数是否一致
bool CheckBlackHeight(Node* node, int blackCount, int benchMark)
{
    if (node == nullptr)
    {
        // 到达叶子节点,检查是否等于基准值
        return blackCount == benchMark;
    }

    if (node->_col == BLACK)
        blackCount++;

    return CheckBlackHeight(node->_left, blackCount, benchMark) &&
           CheckBlackHeight(node->_right, blackCount, benchMark);
}
5.调试和验证

当代码中的某些操作可能会破坏红黑树的性质时,可以调用 IsRBTree() 来验证整个树的合法性,输出不满足性质的详细信息。这些检查帮助确保红黑树操作在每次插入、删除或其他变动后仍符合红黑树的要求。


5. AVL和红黑树的性能比较

AVL树和红黑树是两种经典的自平衡二叉搜索树,它们在不同应用场景中有各自的优势和劣势。下面从几个方面对它们的性能进行对比,包括平衡机制、插入/删除性能、查询性能和实际应用场景。

1. 平衡机制对比

  • AVL树:AVL树严格保持高度平衡,每个节点的左右子树高度差(称为平衡因子)最多为1。为了维持这种严格的平衡,AVL树在插入或删除节点时,几乎每次都需要进行旋转操作以确保平衡。
  • 红黑树:红黑树通过颜色标记(红和黑)以及特定的旋转规则来维持大致的平衡,它的平衡策略相对宽松。红黑树允许路径长度差达到2倍,因此在插入和删除时,它会尽可能避免旋转,除非触发特定条件。

2. 插入性能对比

  • AVL树:由于AVL树保持严格的平衡性,它在插入时往往需要频繁旋转以重新平衡树。插入操作的平均和最差时间复杂度都是 O(log⁡N)O(\log N)O(logN),但旋转操作的频率较高,尤其在接近完全平衡的情况下,这会导致插入操作时间增加。
  • 红黑树:红黑树在插入时通常只需要少量旋转,且更多情况下可以通过染色来维持平衡,而不进行旋转。插入操作的时间复杂度也是 O(log⁡N)O(\log N)O(logN),但由于旋转较少,因此在插入密集的应用场景中,红黑树往往表现得更好。

3. 删除性能对比

  • AVL树:AVL树删除节点后会执行严格的平衡操作,频繁旋转调整确保树高度平衡,因此在删除操作时会消耗更多时间。
  • 红黑树:红黑树的删除操作复杂度相对较低,因为其平衡策略宽松。删除时,红黑树往往可以通过染色操作或少量旋转恢复平衡,不会像AVL树那样频繁旋转。因此在删除操作密集的场景中,红黑树性能通常优于AVL树。

4. 查询性能对比

在查询性能上,二者的时间复杂度都是 O(log⁡N)O(\log N)O(logN),但由于它们的平衡策略不同,有以下几点差异:

  • AVL树:因为严格平衡,AVL树的高度始终接近 log⁡N\log NlogN,这使得查找路径较短。理论上,AVL树的查找性能略优于红黑树,特别是在数据分布接近最差情况时,AVL树会比红黑树更快。
  • 红黑树:红黑树允许路径长度差达到2倍,意味着它的高度可能比AVL树稍大一些。因此在某些场景下,AVL树的查询可能略微快于红黑树。然而,在实际应用中,这种差距通常微乎其微。

5. 内存占用对比

  • AVL树:AVL树每个节点除了存储左右子节点,还要存储平衡因子。因此内存消耗会稍微多一点。
  • 红黑树:红黑树每个节点只需一个颜色标记(红或黑),因此在内存开销上相对较小。

6. 实际应用场景对比

  • AVL树适用场景:AVL树适合频繁查找、不频繁更新(插入/删除)的场景。例如,适合用于静态数据库或对性能要求较高的读取密集型场景。
  • 红黑树适用场景:红黑树适合需要频繁插入和删除操作的场景,特别是在数据动态变化的情况下。红黑树的宽松平衡机制在插入、删除密集的情况下能够减少旋转,从而提升性能。这使得红黑树广泛应用于系统中的许多集合操作,例如C++ STL的 mapset 以及Linux内核中的调度器等。

7. 性能测试示例

假设插入和删除大量数据的性能测试,结果通常会显示:

  • 插入/删除:红黑树的插入和删除操作时间较短,尤其在大量数据的随机插入/删除测试中,红黑树的性能比AVL树好。
  • 查询:查询操作性能上,两者差距不大,AVL树可能在极端情况下稍快,但在实际应用中效果差别不大。

总结

属性AVL树红黑树
平衡机制严格平衡,更多旋转宽松平衡,染色+少量旋转
插入性能较多旋转,插入稍慢较少旋转,插入较快
删除性能较多旋转,删除稍慢较少旋转,删除较快
查询性能高度接近完全平衡,稍快高度略大于AVL,性能接近
内存开销较高(存储平衡因子)较低(存储颜色标记)
适用场景查找频繁、更新较少的场景插入/删除频繁的动态场景

总体而言,红黑树在插入和删除密集的动态应用场景中表现更好,而AVL树在静态或查找频繁的场景中性能稍优。

完整代码:

#pragma once
#include<iostream>
#include<map>
#include<assert.h>
#include<vector>
using namespace std;

enum Color
{
	RED,
	BREAK
};

template<class K, class V>
struct RBTreeNode
{
	struct RBTreeNode<K, V>* _left;
	struct RBTreeNode<K, V>* _right;
	struct RBTreeNode<K, V>* _parent;

	pair<K, V> _kv;
	int _color;

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

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->_color = BREAK;
			return true;
		}
		else
		{
			Node* cur = _root;
			Node* parent = _root;
			while (cur)//比较Frist
			{
				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);
			if (cur->_kv.first < parent->_kv.first)
			{
				parent->_left = cur;
				cur->_parent = parent;
			}
			else
			{
				parent->_right = cur;
				cur->_parent = parent;
			}

			while (parent && parent->_color == RED)
			{
				Node* grandfather = parent->_parent;
				if (parent == grandfather->_right)//父亲为右
				{
					Node* Uncle = grandfather->_left;
					if (Uncle && Uncle->_color == RED)
					{
						parent->_color = Uncle->_color = BREAK;
						grandfather->_color = RED;
						
						cur = grandfather;
						parent = cur->_parent;
					}
					else
					{
							if (cur == parent->_right)
							{
								RotateL(grandfather);
								grandfather->_color = RED;
								parent->_color = BREAK;
							}
							else//交叉
							{
								RotateRL(grandfather);
								grandfather->_color = RED;
								cur->_color = BREAK;
							}
							
					}
				}
				else//父亲在左;
				{
					Node* Uncle = grandfather->_right;
					if (Uncle && Uncle ->_color== RED)
					{
						parent->_color = Uncle->_color = BREAK;
						grandfather->_color = RED;

						cur = grandfather;
						parent = cur->_parent;
					}
					else//叔叔不存在,或者等于黑色,我直线或者弯曲
					{
						if (cur == cur->_left)
						{
							RotateR(grandfather);
							parent->_color = BREAK;
							grandfather->_color = RED;
						}
						else
						{
							RotateLR(grandfather);
							cur->_color = BREAK;
							grandfather->_color = RED;
						}
					}
				}
				_root->_color = BREAK;

			}

			return true;
		}
	}

		void RotateL(Node * parent)//穿的是问题节点,
		{
			Node* cur = parent->_right;
			Node* curleft = cur->_left;

			parent->_right = curleft;

			cur->_left = parent;


			if (curleft)
			{
				curleft->_parent = parent;
			}

			Node* pphead = parent->_parent;

			parent->_parent = cur;

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

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

			cur->_right = parent;

			parent->_left = curright;

			if (curright)
			{
				curright->_parent = parent;
			}

			Node* phead = parent->_parent;

			parent->_parent = cur;

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

		}

		void RotateRL(Node * parent)//传谁谁父亲下去
		{
			Node* cur = parent->_right;

			Node* curleft = cur->_left;


			RotateR(parent->_right);

			RotateL(parent);

		}

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

			Node* curright = cur->_right;


			RotateL(cur);

			RotateR(parent);
		}
		bool IsBalanceTree()
		{
			return IsBalanceTree(_root);
	    }

		bool IsBalanceTree(Node* root)
		{
			if (root == nullptr)
			{
				return true;
			}

			if (root->_color != BREAK)
			{
				return false;
			}

			int benchmark = 0;
			Node* cur = root;
			while (cur)
			{
				if (cur->_color == BREAK)
				{
					benchmark++;
				}
				cur = cur->_left;
			}

			return CheckColour(root, 0, benchmark);
		}

		bool CheckColour(Node* root, int blacknum, int benchmark)
		{
			if (root == nullptr)
			{
				if (blacknum != benchmark)
				{
					return false;
				}
				return true;
			}

			if (root->_color == BREAK)
			{
				blacknum++;
			}

			if (root->_color == RED && root->_parent && root->_parent->_color == RED)
			{
				cout << root->_kv.first << "出现连续红节点" << endl;
			}

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

private:
	Node* _root = nullptr;
};


http://www.kler.cn/a/396987.html

相关文章:

  • 猎板PCB罗杰斯板材的应用案例
  • 什么是PostgreSQL,有什么特点
  • Spring Boot3 实战案例合集上线了
  • 深度神经网络DNN反向传播BP算法公式推导
  • JS 数组排序
  • AI开发-计算机视觉库-OpenCV
  • 新增支持Elasticsearch数据源,支持自定义在线地图风格,DataEase开源BI工具v2.10.2 LTS发布
  • Godot的开发框架应当是什么样子的?
  • .NET 9 中 IFormFile 的详细使用讲解
  • ubuntu16.04配置网卡
  • Python毕业设计选题:基于django+vue的二手物品交易系统
  • (一)机器学习、深度学习基本概念简介
  • 鸿蒙next版开发:使用HiDebug获取调试信息(ArkTS)
  • ini-parser 使用
  • 大模型(LLMs)微调篇
  • Linux 用户账户信息配置文件详解
  • 快速认识和上手Protobuf
  • 无插件H5播放器EasyPlayer.js网页web无插件播放器选择全屏时,视频区域并没有全屏问题的解决方案
  • 如何使用 XML Schema
  • 从0安装mysql server
  • flask+vue使用jwt验证
  • 躺平成长-人工智能进行编程-(12)
  • notepad++下载安装教程
  • SpringBoot项目实现登录——集成JWT令牌和验证码的登录业务
  • 网络安全审计
  • 什么是SSL VPN?其中的协议结构是怎样的?