【数据结构】红黑树

目录

[1 红黑树的概念](#1 红黑树的概念)

[2 红黑树与AVL树的对比](#2 红黑树与AVL树的对比)

[3 红黑树结点的定义](#3 红黑树结点的定义)

[4 红黑树的插入操作](#4 红黑树的插入操作)

[5 红黑树的检验](#5 红黑树的检验)


1 红黑树的概念

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

以下是红黑树的规则:

规则1:每个节点不是红色就是黑色。

这是基础,每个节点都有一个颜色属性。

规则2:根节点必须是黑色。

这条规则很简单,它确保了树的顶端是黑色的。在插入操作时,如果新节点成为了根节点,我们必须把它染成黑色。

规则3:所有叶子节点(NIL节点)都是黑色的。

这里的"叶子节点"指的是红黑树中所有为 NULL 的空指针。为了简化边界条件的处理,我们把这些 NULL 节点视为黑色的、不存储数据的叶子节点。

规则4:红色节点的两个子节点都必须是黑色的。(即不能有两个连续的红色节点)

这是最关键的一条规则。它确保了从任何路径上都不会出现连续的红色节点。

推论:这意味着一条路径上红色节点的数量不会超过黑色节点数量的一半。

规则5:从任意一个节点到其所有后代叶子节点(NIL节点)的路径上,包含相同数量的黑色节点。

这个数量被称为该节点的 黑高

这条规则是平衡性的核心保证。它确保了没有一条路径会比其他路径长出一倍以上。

思考:为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点
个数的两倍

关键就在于 规则4 和 规则5 的结合:

规则5 规定:最长路径和最短路径的黑色节点数量是相同的。

规则4 规定:红色节点不能连续,所以在一条路径上,红色节点只能穿插在黑色节点之间。

最坏情况下的路径长度:

最短路径:可能全部由黑色节点组成。

最长路径:必须是黑红交替的(因为不能连续红,且以黑色节点结束,因为 NIL 是黑的)。

假设黑高为 h(即每条路径有 h 个黑色节点):

最短路径长度 ≈ h(全黑)。

最长路径长度 ≈ 2h(黑红交替)。

因此,最长路径不会超过最短路径的两倍。这种"相对平衡"确保了树的高度始终维持在 O(log n) 级别。

2 红黑树与AVL树的对比

红黑树比AVL树更优。如果有 10 亿个数据,AVL 树的高度最大是 30,从最坏的角度看,红黑树的高度也就 60 ,cpu 查找 30 次与查找 60 消耗的时间差值简直可以忽略不计(就像 1 毛钱与 2 毛钱的关系一样)。所以 红黑树与 AVL 树在效率上是大同小异的,但 AVL 树为了控制严格平衡,在插入和删除时进行了大量的旋转操作,而红黑树由于放宽了平衡条件,使得它在插入和删除时所需的旋转操作的次数更少。

特性 红黑树 AVL 树
平衡标准 通过颜色规则保证从根到叶子的最长路径不超过最短路径的2倍 严格平衡,任何节点的左右子树高度差绝对值不超过1
查找性能 平均稍慢,因为树可能没有AVL那么平衡 更优,尤其是在查找密集型任务中
插入/删除性能 更优,需要的旋转操作更少,维护开销更低 相对较慢,可能需要更多旋转来维持严格平衡
适用场景 需要频繁插入、删除的场景(如数据库索引、内存分配器) 需要频繁查找而插入删除较少的场景(如字典)
存储开销 每个节点需要1 bit存储颜色信息(红/黑) 每个节点需要存储平衡因子(通常为2 bits或一个整型)

3 红黑树结点的定义

cpp 复制代码
// 节点的颜色
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;               // 节点的颜色
};

思考:在节点的定义中,为什么要将节点的默认颜色给成红色的?

规则4:红色节点的两个子节点都必须是黑色的。(即不能有两个连续的红色节点)

规则5:从任意一个节点到其所有后代叶子节点(NIL节点)的路径上,包含相同数量的黑色节点。

插入红色结点,可能会违反规则4,一定不会违反规则 5;插入黑色结点,可能会违反规则5,一定不会违反规则 4。违反了规则 5 要付出的代价比违反规则 4 付出的代价要高

为了后续实现关联式容器简单,红黑树的实现中增加一个头结点,因为根节点必须为黑色,为了

与根节点进行区分,将头结点给成黑色,并且让头结点的 pParent 域指向红黑树的根节点,pLeft

域指向红黑树中最小的节点,_pRight域指向红黑树中最大的节点,如下:

4 红黑树的插入操作

红黑树是在二叉搜索树的基础上加上其平衡限制条件,因此红黑树的插入可分为两步:

1. 按照二叉搜索的树规则插入新节点

cpp 复制代码
template<class ValueType>
class RBTree
{
    //......
    bool Insert(const ValueType& data)
    {
        PNode& pRoot = GetRoot();
        if (nullptr == pRoot)
        {
            pRoot = new Node(data, BLACK);
            // 根的双亲为头节点
            pRoot->_pParent = _pHead;
            _pHead->_pParent = pRoot;
        }
        else
        {
            // 1. 按照二叉搜索的树方式插入新节点
                        // 2. 检测新节点插入后,红黑树的性质是否造到破坏,
            //   若满足直接退出,否则对红黑树进行旋转着色处理
        }
        // 根节点的颜色可能被修改,将其改回黑色
        pRoot->_color = BLACK;
        _pHead->_pLeft = LeftMost();
        _pHead->_pRight = RightMost();
        return true;
    }
private:
    PNode& GetRoot() { return _pHead->_pParent; }
    // 获取红黑树中最小节点,即最左侧节点
    PNode LeftMost();
    // 获取红黑树中最大节点,即最右侧节点
    PNode RightMost();
private:
    PNode _pHead;
}

2. 新节点插入后,检测红黑树的性质是否造到破坏

因为新节点的默认颜色是红色 ,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何

性质,则不需要调整,插入结束;但当新插入节点的双亲节点颜色为红色时,就违反了规则4(不能有连在一起的红色节点),此时需要对红黑树分情况来讨论:

约定:cur为当前节点(不一定就是插入的结点,也可能是调整过程的结点),p为父节点,g为祖父节点,u为叔叔节点,下面的二叉树可能是一颗完整的树,也可能是一颗子树,三角形代表一颗子树。

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

解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整

继续向上调整:

如果g是根节点,调整完成后,将g改为黑色,插入结束

如果g是子树,且g的双亲如果是黑色,插入结束

如果g是子树,且g的双亲如果是红色,需要继续向上调整
情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑

(有趣的推断:1、如果 u 存在且为黑,那么 cur 一定不是新插入的结点,因为如果 cur 是新插入的结点,那么在插入之前这颗树就不满足规则 5。)

子情况1:

  • 如果 p 为 g 的左孩子,cur 为 p 的左孩子,则对 g 进行右单旋转
  • 相反如果 p 为 g 的右孩子,cur 为 p 的右孩子,则对 g 进行左单旋转

然后:p、g变色--p变黑,g变红,插入结束

子情况2:

  • 如果 p 为 g 的左孩子,cur为 p 的右孩子,则对 p 进行左单旋转
  • 相反如果 p 为 g 的右孩子,cur为 p 的左孩子,则针对 p 进行右单旋转

然后交换 p 和 cur 指针,则转换成了子情况1

逻辑梳理:

cpp 复制代码
	bool Insert(const T& data)
	{
		if (_root == nullptr)
		{
			_root = new Node(data, BLACK);
			return true;
		}
		else
		{
			Node* cur = _root;
			Node* parent = nullptr;

			while (cur)
			{
				if (cur->_data > data)
				{
					parent = cur;
					cur = cur->_left;
				}
				else if (cur->_data < data)
				{
					parent = cur;
					cur = cur->_right;
				}
				else return false;
			}

			cur = new Node(data);
			if (parent->_data > data) parent->_left = cur;
			else parent->_right = cur;
			cur->_parent = parent;

			while (parent->_col == RED)
			{
				Node* g = parent->_parent;
				Node* uncle = nullptr;
				if (parent == g->_left) uncle = g->_right;
				else uncle = g->_left;

				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					g->_col = RED;
					if (g == _root)
					{
						g->_col = BLACK;
						return true;
					}
					if (g->_parent && g->_parent->_col == BLACK) return true;
					cur = g;
					parent = cur->_parent;
				}
				else
				{
					if (parent == g->_left)
					{
						if (cur == parent->_left)
						{
							rotateR(g);
							parent->_col = BLACK;
							g->_col = RED;

							if (g == _root) g->_col = BLACK;
							return true;
						}
						else if (cur == parent->_right)
						{
							rotateL(parent);
							swap(parent, cur);
						}
					}
					else if (parent == g->_right)
					{
						if (cur == parent->_right)
						{
							rotateL(g);
							parent->_col = BLACK;
							g->_col = RED;

							if (g == _root) g->_col = BLACK;
							return true;
						}
						else if (cur == parent->_left)
						{
							rotateR(parent);
							swap(parent, cur);
						}
					}
				}
			}
		}

		return true;
	}

5 红黑树的检验

对于一颗二叉树,只要它满足1、二叉搜索树的性质 2、红黑树的规则,那么它就是一个合法的红黑树。由于红黑树是按照二叉搜索树的规则插入结点的,所以它一定满足二叉搜索树的性质,接下来看它是否满足红黑树的规则:

规则1:每个节点不是红色就是黑色。

cpp 复制代码
// 节点的颜色
enum Color { RED, BLACK };

颜色是用枚举体定义,一定满足

规则2:根节点必须是黑色。

cpp 复制代码
if (root->_col != BLACK)
{
	return false;
}

规则3:所有叶子节点(NIL节点)都是黑色的。

这是定义,不需要检查

规则4:红色节点的两个子节点都必须是黑色的。(即不能有两个连续的红色节点)

检验这条规则时,最好不要检查孩子结点,因为还要讨论孩子结点是否存在。可以检查红色结点的父结点是否是红色结点。

cpp 复制代码
if (root->_col == RED && root->_parent && root->_parent->_col == RED)
{
	cout << root->_kv.first << "出现连续红色节点" << endl;
	return false;
}

规则5:从任意一个节点到其所有后代叶子节点(NIL节点)的路径上,包含相同数量的黑色节点。

将待检查的二叉树的最左边的路径的黑色结点数量作为基准值,检查其他路径的黑色结点数量是否等于这个基准值。

cpp 复制代码
// 基准值
int benchmark = 0;
Node* cur = _root;
while (cur)
{
	if (cur->_col == BLACK)
		++benchmark;

	cur = cur->_left;
}

将规则 4 和 5 的检查放在二叉树的前序遍历里,在遍历的同时,再增加一个变量存储从根结点到当前结点的黑色结点的数量,如果遍历到空结点了,代表一条路径的结束,此时检查规则 5。存储从根结点到当前结点的黑色结点的数量的那个变量不要设置成引用类型,不然就表示的是整颗二叉树的黑色结点的数量了。

完整代码:

cpp 复制代码
	bool IsRBTree()
	{
		return IsRBTree(_root); // 函数重载
	}

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

		if (root->_col != BLACK)
		{
			return false;
		}

		// 基准值
		int benchmark = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				++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->_col == BLACK)
		{
			++blacknum;
		}

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

		return CheckColour(root->_left, blacknum, benchmark)
			&& CheckColour(root->_right, blacknum, benchmark);
	}
相关推荐
饼瑶2 小时前
基于AutoDL远端服务复现具身智能论文OpenVLA
算法
Mr.Winter`2 小时前
无人船 | 图解基于MPC控制的路径跟踪算法(以全驱动无人艇WAMV为例)
人工智能·算法·机器人·自动驾驶·ros·路径规划
咪咪渝粮2 小时前
112.路径总和
java·数据结构·算法
高洁012 小时前
大模型-详解 Vision Transformer (ViT) (2
深度学习·算法·aigc·transformer·知识图谱
电子_咸鱼2 小时前
高阶数据结构——并查集
数据结构·c++·vscode·b树·python·算法·线性回归
zl_dfq2 小时前
基于哈夫曼树的数据压缩算法
算法
多喝开水少熬夜2 小时前
算法-哈希表和相关练习-java
java·算法·散列表
xiaoye-duck3 小时前
数据结构之二叉树-堆
数据结构
余衫马3 小时前
聚类算法入门:像魔法一样把数据自动归类
人工智能·算法·机器学习·聚类