【C++】红黑树为什么比AVL快?用C++亲手实现告诉你答案


【C++】红黑树为什么比AVL快?用C++亲手实现告诉你答案

  • 摘要
  • 目录
    • 一、红黑树的概念
      • [1. 红黑树的定义](#1. 红黑树的定义)
      • [2. 红黑树的规则](#2. 红黑树的规则)
      • [3. 红黑树如何保证最长路径不超过最短路径的 2 倍?](#3. 红黑树如何保证最长路径不超过最短路径的 2 倍?)
      • [4. 红黑树的效率](#4. 红黑树的效率)
    • 二、红黑树的实现
      • [1. 节点的设计与实现](#1. 节点的设计与实现)
      • [2. 红黑树类的设计](#2. 红黑树类的设计)
      • [3. 插入一个值](#3. 插入一个值)
      • [3.1 变色](#3.1 变色)
      • [3.2 单旋+变色](#3.2 单旋+变色)
      • [3.3 双旋+变色](#3.3 双旋+变色)
      • [3.4 调整总代码](#3.4 调整总代码)
    • 三、验证
      • [1. 平衡验证](#1. 平衡验证)
      • [2. 高度计算](#2. 高度计算)
    • 四、示例测试
    • 总代码
  • 总结

摘要

红黑树作为一种高效的自平衡二叉搜索树,通过巧妙的颜色约束规则在插入和删除时以更少的旋转操作维持树的近似平衡。本文从红黑树的核心概念出发,详细解析了其五大平衡规则和效率保证机制,并通过完整的C++代码实现展示了插入操作中的变色、单旋和双旋等平衡调整策略。与严格的AVL树相比,红黑树通过允许一定程度的高度不平衡,在实际应用中实现了更优的整体性能,成为C++ STL中map和set等关联容器的底层实现基础。


目录

一、红黑树的概念

1. 红黑树的定义

红黑树是一种二叉搜索树(BST),每个结点额外维护一位颜色信息:红色或黑色。通过对从根到任一叶子的路径上结点颜色施加约束从而保证了任意两条根到叶子的路径长度差不会超过常数因子,从而保持近似平衡,确保查找、插入和删除等基本操作在对数时间内完成。

2. 红黑树的规则

红黑树是一种在二叉搜索树基础上增加颜色约束的自平衡结构,为了保持树的近似平衡,它必须满足以下规则:

  1. 每个结点非红即黑。
  2. 根结点始终为黑色。
  3. 若一个结点为红色,则它的两个子结点必须为黑色。
    换言之,红色结点不能连续出现,即任意路径上不存在相邻的红色结点。
  4. 从任意结点出发,到其所有 NULL(空)结点的路径上,黑色结点数量相同。

补充说明:

在《算法导论》等经典书籍中,额外增加了一条规则:每个叶子结点(NIL)都是黑色的。

这里的"叶子结点"并非我们通常意义上的末端结点,而是指用于标识空指针位置的特殊"外部结点"。这些 NIL 结点的存在仅用于描述和推理红黑树的性质,实际实现中往往被省略或忽略,只需理解这一概念即可。




3. 红黑树如何保证最长路径不超过最短路径的 2 倍?

  • 根据 规则 4 ,从根到任意 NULL 结点的路径上,黑色结点数量相同 。

    因此,极端情况下最短路径就是全黑结点路径 ,设该路径长度为 bh(black height)。

  • 根据 规则 2 和 3 ,任意路径上不会出现连续的红色结点。

    所以最长的路径只能是"黑红相间"的形式,其长度为 2 * bh。

  • 由此可知,若任意从根到 NULL 的路径长度为 h,则满足:

    复制代码
    bh ≤ h ≤ 2 * bh

    即最长路径不会超过最短路径的两倍,从而保证红黑树始终保持"近似平衡"。


4. 红黑树的效率

设红黑树中共有 N 个结点,最短路径长度为 bh,则结点数与高度的关系满足:

复制代码
2^bh - 1 ≤ N < 2^(2*bh) - 1

由此可得:

复制代码
h ≈ 2 * log₂N

这意味着红黑树在最坏情况下的操作(查找、插入、删除)都至多沿着最长路径执行,

因此其时间复杂度仍为:

复制代码
O(log N)

二、红黑树的实现

1. 节点的设计与实现

红黑树是一种自平衡的二叉搜索树,核心在于通过节点的颜色和一系列规则来维持树的平衡。所以我们首先需要设计树的节点结构。

  • 数据储存: 采用类模板实现数据储存,支持泛型编程。使用std::pair<K,V>来储存键对值_kv,这使得我们的红黑树能够像map一样关联一个键和一个值。
  • 节点颜色: 使用枚举类型Color来定义颜色,将颜色状态限定为RED 或者BALACK,为了确保类型的安全性,优于使用无意义的布尔或者整数。
  • 节点结构: 采用三叉链结构,既包含指向父节点的指针,又包含指向左、右孩子的指针。这对红黑树的旋转和回溯调整操作至关重要。
  • 访问权限: 节点内的成员变量(指针,颜色)需要被红黑树类频繁的访问和修改,所以我们可以使用struct定义节点,使其成员默认公有。
cpp 复制代码
//使用枚举类定义节点颜色
enum Color {
	RED,
	BLACK
};

//红黑树节点类模板
template<class K, class V>
struct RBTreeNode {
	//储存键对值数据
	pair<K, V> _kv; 
	
	//三叉链指针
	RBTreeNode<K, V> _parent;
	RBTreeNode<K, V> _left;
	RBTreeNode<K, V> _right;

	//节点颜色
	Color _col;

	//构造函数
	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _parent(nullptr)
		, _left(nullptr)
		, _right(nullptr)
		//新节点初始颜色为红
		,_col(RED)
	{ }
};

那么我们为什么初始化的时候将新节点的颜色初始化为红色?

新插入的节点初始颜色设置为 红色 是一个战略性选择。如果新节点默认为黑色,那么它所在的路径会立即比其他路径多出一个黑色节点,直接破坏了红黑树 "从任一节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点" 这一关键性质。调整起来会非常困难。而插入红色节点可能破坏的是"红色节点的子节点必须是黑色"的性质,这种情况可以通过局部的颜色调整和旋转来解决,影响范围更小,修正策略更可控。虽然在后续的插入调整逻辑中会显式地设置颜色,但将初始值设为红色符合红黑树插入操作的标准实践。


2. 红黑树类的设计

节点结构定义完成后,我们需要构建红黑树的本体类,它用来封装所有对树的操作,并持有整棵树的根节点。

  • 封装与数据隐藏:使用class来定义红黑树,将根节点_root·等关键数据成员设置成private,防止外部直接修改,破坏数据结构的完整和安全性。
  • 类型别名:在红黑树类的内部,我们使用typedf为冗长的节点类型RBTreeNode<K,V>创建一个更简短的别名Node,提高了代码的可读性和可维护性。
  • 初始化:在构造函数中,将根节点_root初始化为nullptr,表示初始状态为一棵空树。
cpp 复制代码
template<class K, class V>
class RBTree {
	typedef RBTreeNode<K, V> Node;
public:
	RBTree()
		:_root(nullptr)
	{ }

	//后续操作......

private:
	Node* _root;
};

3. 插入一个值

首先我们插入一个值是按照二叉搜索树的插入规则进行插入,擦汗如后我们需要观察是否否和红黑树的规则。

  • 如果插入的时候是空树,这个插入的节点就会做为红黑树的根节点,所以这个根节点的颜色必须是黑色。
  • 如果插入的时候不是空树,新增节点的颜色必须是红色,然后我们观察父节点是否是黑色,,如果是不违反任何红黑树的规则,插入结束;如果父节点是红色,我们就要分情况处理,我们往下分析。
cpp 复制代码
//后续操作......
bool Insert(const pair<K, V>& kv)
{
	//根节点为空
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}
	else//根节点不为空插入
	{
		Node* parent = nullptr;
		Node* cur = _root;
		//找插入位置
		while (cur != nullptr)
		{
			if (kv.first > cur->_kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else if (kv.first < cur->_kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else { return false; }
		}
		//找到位置进行插入并给颜色和更新父节点
		cur = new Node(kv);
		cur->_col = RED;
		cur->_parent = parent;
		//与父节点进行链接
		if (cur->_kv.first > parent->_kv.first) { parent->_right = cur; }
		else { parent->_left = cur; }
		
		//根据不同情况调整红黑树的平衡......
	}
}

红黑树是基于二叉搜索树实现的,所以为了维持红黑树的平衡,我们需要进行的旋转操作和前文的AVL树的旋转操作类似,不同的是不需要更新维持平衡因子,我们先更新修改一下左单旋和右单旋旋转操作的代码,并限定为私有成员函数。

cpp 复制代码
//右单旋
void RotateR(Node* parent)
{
	Node* ppnode = parent->_parent;
	Node* cur = parent->_left;
	Node* curR = cur->_right;

	parent->_left = curR;
	if (curR) { curR->_parent = parent; }

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

	if (ppnode == nullptr)
	{
		_root = cur;
		cur->_parent = nullptr;
	}
	else
	{
		if (ppnode->_left == parent)
		{
			ppnode->_left = cur;
			cur->_parent = ppnode;
		}
		else
		{
			ppnode->_right = cur;
			cur->_parent = ppnode;
		}
	}
}
//左单旋
void RotateL(Node* parent)
{
	Node* ppnode = parent->_parent;
	Node* cur = parent->_right;
	Node* curL = cur->_left;

	parent->_right = curL;
	if (curL) { curL->_parent = parent; }

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

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

3.1 变色

我们设新增的节点是cur( c ),它的父节点是parent( p ),父节点的父节点为grandfather( g ),父节点的兄弟为uncle( u )。

当新插入的红色节点 cur 其父节点 parent 也为红色时,便构成了"双红缺陷",直接违反了红黑树禁止连续红色节点的核心规则,此时必须进行干预调整。这一问题的出现蕴含着明确的先决条件:由于 parent 为红色,根据红黑树根节点必黑且红色节点不能连续的性质,可以明确推断出 parent 绝不可能是根节点。因此,插入节点 cur 必然存在祖父节点 grandfather,且其颜色必定为黑色。

在整个调整逻辑中,叔叔节点 uncle(即 parent 的兄弟节点)扮演着决定性角色。uncle 的存在与否及其具体颜色,是划分不同处理情况并决定后续调整策略的唯一依据。整个修复机制将围绕对 uncle 状态的判断而展开。

叔叔存在且为红

当叔叔节点 uncle 存在且颜色为红色时,核心矛盾在于局部出现了两个连续的红色节点,但幸运的是,仅通过颜色变换 即可有效解决此问题,无需改变树的物理结构。具体的操作步骤是将父节点 parent 和叔叔节点 uncle 均由红色变为黑色,同时将祖父节点 grandfather 由黑色变为红色。

这一系列颜色变换的背后有着严谨的逻辑支撑。将 parent 变黑直接切断了 parent 与 cur 之间的连续红色链,解决了最紧迫的规则冲突。然而,parent 由红变黑会使得通过 parent 的路径黑色节点数量增加,破坏了黑高平衡。为此,将 grandfather 由黑变红,恰好抵消了该路径黑高的增加。但 grandfather 变红又导致通过 uncle 的路径黑高降低,因此必须同步将 uncle 由红变黑,从而使得整棵以 grandfather 为根的子树在插入调整后恢复原有的黑高平衡。

完成本次颜色调整后,情况变得更为复杂。如果 grandfather 恰好是整棵树的根节点,我们可以在算法末尾统一将根节点重置为黑色即可满足规则。但更常见的情形是 grandfather 并非根节点,此时它被置为红色后,可能与其自身的父节点形成新的双红缺陷,导致问题向上传播。因此,必须将 grandfather 设置为新的当前节点 cur,继续向上进行平衡检查与调整。这一过程需要在一个以 parent 存在且为红色为条件的循环中执行,以处理可能发生的连续向上调整,并在最终确保根节点颜色为黑色。

这种情况的处理具有一个显著特点:其调整策略与当前节点 cur 是其父节点 parent 的左孩子还是右孩子完全无关,处理方式完全统一。虽然颜色调整可能导致问题向上层传播,但每次传播都至少上升两层,这确保了整个修复过程的时间复杂度能够被控制在对数级别,维持了红黑树的高效平衡特性。

cpp 复制代码
// 当父节点存在且为红色时,进入循环调整
while (parent && parent->_col == RED)
{
	Node* grandfather = parent->_parent;
	//当父节点是祖父节点的左孩子
	if (grandfather->_left == parent)
	{
		Node* uncle = grandfather->_right;
		if (uncle && uncle->_col == RED)//叔叔节点存在且为红色
		{
			//更新颜色
			parent->_col = BLACK;
			uncle->_col = BLACK;
			grandfather->_col = RED;

			//继续向上查找
			cur = grandfather;
			parent = cur->_parent;
		}
		else//叔叔节点不存在或者颜色不为红色,需要进行旋转
		{

			break;
		}
	}
	else//当父节点是祖父节点的右孩子
	{
		Node* uncle = grandfather->_left;
		if (uncle && uncle->_col == RED)//叔叔节点存在且为红色
		{
			//更新颜色
			parent->_col = BLACK;
			uncle->_col = BLACK;
			grandfather->_col = RED;

			//继续向上查找
			cur = grandfather;
			parent = cur->_parent;
		}
		else//叔叔节点不存在或者颜色不为红色,需要进行旋转
		{

			break;
		}
	}
}
_root->_col = BLACK;

3.2 单旋+变色

当处理叔叔节点不存在或为黑色的情况时,我们需要通过旋转结合变色来恢复红黑树的平衡。这种情况发生在父节点为红色而叔叔节点无法提供平衡支持时。

当叔叔节点不存在或为黑色 ,且当前节点与父节点、父节点与祖父节点形成直线型结构时,采用单旋配合变色的调整策略。根据具体的结构方向,分为两种对称情况:

情况一:左左直线型

当父节点位于祖父节点的左侧,且当前节点也位于父节点的左侧时,形成左左直线结构。此时以祖父节点为旋转点执行右单旋。旋转完成后,将提升为子树根节点的原父节点颜色改为黑色,将原祖父节点颜色改为红色。这样的颜色调整既消除了连续的红色节点冲突,又确保了所有路径的黑色节点数量保持不变。

cpp 复制代码
if (parent->_left == cur)//左左直线型
{
				RotateR(grandfather);
				parent->_col = BLACK;
				grandfather->_col = RED;
}

情况二:右右直线型

当父节点位于祖父节点的右侧,且当前节点也位于父节点的右侧时,形成右右直线结构。这是左左情况的完美对称。此时以祖父节点为旋转点执行左单旋。旋转完成后,同样将提升后的原父节点颜色改为黑色,原祖父节点颜色改为红色。

cpp 复制代码
	if (parent->_right == cur)//右右直线型
				{
					RotateL(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}

这两种单旋处理具有相同的效果:通过一次旋转直接修正树的结构失衡,配合精确的颜色变换,在解决双红缺陷的同时维持了红黑树的平衡性质。由于调整后的子树根节点变为黑色,不会与上层节点形成新的颜色冲突,因此调整过程到此结束。

3.3 双旋+变色

当叔叔节点不存在或为黑色,且当前节点与父节点、父节点与祖父节点形成折线型结构时,需要采用双旋配合变色的调整策略。这种情况需要两次旋转来将折线结构转换为平衡状态。

情况一:左右折线型

当父节点位于祖父节点的左侧,而当前节点位于父节点的右侧时,形成左右折线结构。此时需要先以父节点为旋转点执行左单旋 ,将折线结构转换为左左直线结构。完成第一次旋转后,再以祖父节点为旋转点执行右单旋。在颜色调整阶段,将当前节点颜色改为黑色,原祖父节点颜色改为红色。

cpp 复制代码
else//左右折现行
{
				RotateL(parent);
				RotateR(grandfather);
				cur->_col = BLACK;
				grandfather->_col = RED;
}

情况二:右左折线型

当父节点位于祖父节点的右侧,而当前节点位于父节点的左侧时,形成右左折线结构。这是左右情况的完美对称。此时需要先以父节点为旋转点执行右单旋 ,将折线结构转换为右右直线结构。完成第一次旋转后,再以祖父节点为旋转点执行左单旋。在颜色调整阶段,同样将当前节点颜色改为黑色,原祖父节点颜色改为红色。

这两种双旋处理的核心在于通过两次方向相反的旋转操作,将复杂的折线结构重新调整为平衡状态。精心设计的颜色变换确保了在解决双红缺陷的同时,所有路径的黑色节点数量保持严格一致。调整后的子树根节点(即当前节点)变为黑色,确保了不会与上层节点产生新的颜色冲突,因此调整过程完全结束。

cpp 复制代码
else//右左折线型
{
				RotateR(parent);
				RotateL(grandfather);
				cur->_col = BLACK;
				grandfather->_col = RED;
}

3.4 调整总代码

cpp 复制代码
// 当父节点存在且为红色时,进入循环调整
while (parent && parent->_col == RED)
{
	Node* grandfather = parent->_parent;
	//当父节点是祖父节点的左孩子
	if (grandfather->_left == parent)
	{
		Node* uncle = grandfather->_right;
		if (uncle && uncle->_col == RED)//叔叔节点存在且为红色
		{
			//更新颜色
			parent->_col = BLACK;
			uncle->_col = BLACK;
			grandfather->_col = RED;

			//继续向上查找
			cur = grandfather;
			parent = cur->_parent;
		}
		else//叔叔节点不存在或者颜色为黑色,需要进行旋转
		{
			if (parent->_left == cur)//左左直线型
			{
				RotateR(grandfather);
				parent->_col = BLACK;
				grandfather->_col = RED;
			}
			else//左右折现行
			{
				RotateL(parent);
				RotateR(grandfather);
				cur->_col = BLACK;
				grandfather->_col = RED;
			}
			break;
		}
	}
	else//当父节点是祖父节点的右孩子
	{
		Node* uncle = grandfather->_left;
		if (uncle && uncle->_col == RED)//叔叔节点存在且为红色
		{
			//更新颜色
			parent->_col = BLACK;
			uncle->_col = BLACK;
			grandfather->_col = RED;

			//继续向上查找
			cur = grandfather;
			parent = cur->_parent;
		}
		else//叔叔节点不存在或者颜色为黑色,需要进行旋转
		{
			if (parent->_right == cur)//右右直线型
			{
				RotateL(grandfather);
				parent->_col = BLACK;
				grandfather->_col = RED;
			}
			else//右左折线型
			{
				RotateR(parent);
				RotateL(grandfather);
				cur->_col = BLACK;
				grandfather->_col = RED;
			}
			break;
		}
	}
}

三、验证

1. 平衡验证

  1. 红黑树的平衡验证需要确保其五大性质得到满足。为了在类外部能够方便地调用验证函数,同时不暴露内部根节点指针,我们采用了函数重载的优雅设计。公共接口是一个无参数的 IsBalance() 函数,它内部调用一个接收根节点指针的私有重载版本,从而既保证了封装性,又实现了完整的递归验证。

  2. 验证过程首先检查性质二:根节点必须为黑色。这是整个红黑树性质的基础,如果根节点为红色,则直接判定为不平衡。在确认根节点为黑色后,我们需要为性质四的验证确立一个基准。通过遍历整棵树最左侧的路径,计算出该路径上的黑色节点总数,这个数值将作为所有路径都必须遵守的黑色节点数量标准。

  3. 核心的递归验证函数承担着同时检查性质三和性质四的重任。性质三要求不能出现连续的红色节点,我们通过检查每个节点与其父节点的颜色来确保这一点。如果发现某个红色节点的父节点也是红色,就立即判定为违规。性质四要求所有路径的黑色节点数量相同,我们采用深度优先遍历,在每条路径到达末端时,将累计的黑色节点数量与之前确立的基准值进行比较。任何不一致都意味着树结构不平衡。

  4. 在实现过程中,我们巧妙利用了值传递的特性来维护每条路径独立的黑色节点计数器。每个递归调用栈都拥有自己独立的计数器副本,从而自然实现了路径间的隔离。整个验证过程通过逻辑与操作确保所有子树都必须通过验证,任何子树的失败都会导致整个验证失败。这种设计不仅保证了验证的完整性,还能在发现第一个违规点时立即返回,提高了验证效率。

cpp 复制代码
public:
	// 平衡验证的公共接口
	bool IsBalance()
	{
		return IsBalance(_root);
	}
private:
	// 内部递归验证函数
	bool CheckColour(Node* root, int blacksum, int leftpathblack)
	{
		if (root == nullptr)//这里的意思是判断是否到了根节点的末端
		{
			if (blacksum != leftpathblack)
			{
				cout << "每条路径上的黑色节点的数目不同" << endl;
				return false;
			}
			return true;
		}

		Node* parent = root->_parent;
		if (parent && parent->_col == RED && root->_col == RED)
		{
			cout << root->_kv.first << ':' << "出现两个连续的红节点" << endl;
			return false;
		}

		if (root->_col == BLACK)
			blacksum++;

		return CheckColour(root->_left, blacksum, leftpathblack) &&
			CheckColour(root->_right, blacksum, leftpathblack);
	}

2. 高度计算

cpp 复制代码
// 平衡验证的公共接口
public:
	int Height()
	{
		return Height(_root);
	}
private:
	int Height(Node* root)
	{
		if (root == nullptr) return 0;           // 基线条件:空树高度为0

		int heightL = Height(root->_left);       // 递归计算左子树高度
		int heightR = Height(root->_right);      // 递归计算右子树高度

		return heightL > heightR ? heightL + 1 : heightR + 1;  // 取较大值+1
	}

四、示例测试

cpp 复制代码
void TestRBTree() 
{
	RBTree<int, int> tree;
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };

	for (auto e : a) {
		tree.Insert(make_pair(e, e));
	}

	if (tree.IsBalance()) {
		cout << "红黑树平衡验证通过" << endl;
	}
	else {
		cout << "红黑树不平衡" << endl;
	}

	cout << "树高度: " << tree.Height() << endl;
}

int main()
{
	TestRBTree();
	return 0;
}

总代码

cpp 复制代码
#include<iostream>

using namespace std;

//使用枚举类定义节点颜色
enum Color {
	RED,
	BLACK
};

//红黑树节点类模板
template<class K, class V>
struct RBTreeNode {
	//储存键对值数据
	pair<K, V> _kv; 
	
	//三叉链指针
	RBTreeNode<K, V>* _parent;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;

	//节点颜色
	Color _col;

	//构造函数
	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _parent(nullptr)
		, _left(nullptr)
		, _right(nullptr)
		//新节点初始颜色为红
		,_col(RED)
	{ }
};

//红黑树类模板
template<class K, class V>
class RBTree {
	typedef RBTreeNode<K, V> Node;
private:
	Node* _root;

public:
	RBTree()
		:_root(nullptr)
	{ }

	//后续操作......
	bool Insert(const pair<K, V>& kv)
	{
		//根节点为空
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_col = BLACK;
			return true;
		}
		else//根节点不为空插入
		{
			Node* parent = nullptr;
			Node* cur = _root;
			//找插入位置
			while (cur != nullptr)
			{
				if (kv.first > cur->_kv.first)
				{
					parent = cur;
					cur = cur->_right;
				}
				else if (kv.first < cur->_kv.first)
				{
					parent = cur;
					cur = cur->_left;
				}
				else { return false; }
			}
			//找到位置进行插入并给颜色和更新父节点
			cur = new Node(kv);
			cur->_col = RED;
			cur->_parent = parent;
			//与父节点进行链接
			if (cur->_kv.first > parent->_kv.first) { parent->_right = cur; }
			else { parent->_left = cur; }

			//根据不同情况调整红黑树的平衡......
			
			// 当父节点存在且为红色时,进入循环调整
			while (parent && parent->_col == RED)
			{
				Node* grandfather = parent->_parent;
				//当父节点是祖父节点的左孩子
				if (grandfather->_left == parent)
				{
					Node* uncle = grandfather->_right;
					if (uncle && uncle->_col == RED)//叔叔节点存在且为红色
					{
						//更新颜色
						parent->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//继续向上查找
						cur = grandfather;
						parent = cur->_parent;
					}
					else//叔叔节点不存在或者颜色为黑色,需要进行旋转
					{
						if (parent->_left == cur)//左左直线型
						{
							RotateR(grandfather);
							parent->_col = BLACK;
							grandfather->_col = RED;
						}
						else//左右折现行
						{
							RotateL(parent);
							RotateR(grandfather);
							cur->_col = BLACK;
							grandfather->_col = RED;
						}
						break;
					}
				}
				else//当父节点是祖父节点的右孩子
				{
					Node* uncle = grandfather->_left;
					if (uncle && uncle->_col == RED)//叔叔节点存在且为红色
					{
						//更新颜色
						parent->_col = BLACK;
						uncle->_col = BLACK;
						grandfather->_col = RED;

						//继续向上查找
						cur = grandfather;
						parent = cur->_parent;
					}
					else//叔叔节点不存在或者颜色为黑色,需要进行旋转
					{
						if (parent->_right == cur)//右右直线型
						{
							RotateL(grandfather);
							parent->_col = BLACK;
							grandfather->_col = RED;
						}
						else//右左折线型
						{
							RotateR(parent);
							RotateL(grandfather);
							cur->_col = BLACK;
							grandfather->_col = RED;
						}
						break;
					}
				}
			}
			_root->_col = BLACK;
			return true;
		}
	}

	// 平衡验证的公共接口
	bool IsBalance()
	{
		return IsBalance(_root);
	}

	int Height()
	{
		return Height(_root);
	}

private:

	//右单旋
	void RotateR(Node* parent)
	{
		Node* ppnode = parent->_parent;
		Node* cur = parent->_left;
		Node* curR = cur->_right;

		parent->_left = curR;
		if (curR) { curR->_parent = parent; }

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

		if (ppnode == nullptr)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
				cur->_parent = ppnode;
			}
			else
			{
				ppnode->_right = cur;
				cur->_parent = ppnode;
			}
		}
	}
	//左单旋
	void RotateL(Node* parent)
	{
		Node* ppnode = parent->_parent;
		Node* cur = parent->_right;
		Node* curL = cur->_left;

		parent->_right = curL;
		if (curL) { curL->_parent = parent; }

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

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


	// 内部平衡验证实现
	bool IsBalance(Node* root)
	{
		if (root == nullptr)
			return true;
		//检查根节点是不是黑色
		if (root->_col != BLACK)
		{
			cout << "根节点颜色错误不为黑色" << endl;
			return false;
		}
		//计算最左之路的黑色节点个数
		int leftpathblack = 0;
		Node* cur = root;
		while (cur)
		{
			if (cur->_col == BLACK)
				leftpathblack++;
			cur = cur->_left;
		}

		return CheckColour(root, 0, leftpathblack);
	}

	// 内部递归验证函数
	bool CheckColour(Node* root, int blacksum, int leftpathblack)
	{
		if (root == nullptr)//这里的意思是判断是否到了根节点的末端
		{
			if (blacksum != leftpathblack)
			{
				cout << "每条路径上的黑色节点的数目不同" << endl;
				return false;
			}
			return true;
		}

		Node* parent = root->_parent;
		if (parent && parent->_col == RED && root->_col == RED)
		{
			cout << root->_kv.first << ':' << "出现两个连续的红节点" << endl;
			return false;
		}

		if (root->_col == BLACK)
			blacksum++;

		return CheckColour(root->_left, blacksum, leftpathblack) &&
			CheckColour(root->_right, blacksum, leftpathblack);
	}

	int Height(Node* root)
	{
		if (root == nullptr) return 0;           // 基线条件:空树高度为0

		int heightL = Height(root->_left);       // 递归计算左子树高度
		int heightR = Height(root->_right);      // 递归计算右子树高度

		return heightL > heightR ? heightL + 1 : heightR + 1;  // 取较大值+1
	}

};


void TestRBTree() 
{
	RBTree<int, int> tree;
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };

	for (auto e : a) {
		tree.Insert(make_pair(e, e));
	}

	if (tree.IsBalance()) {
		cout << "红黑树平衡验证通过" << endl;
	}
	else {
		cout << "红黑树不平衡" << endl;
	}

	cout << "树高度: " << tree.Height() << endl;
}

int main()
{
	TestRBTree();
	return 0;
}

总结

通过本文的系统讲解和完整代码实现,我们深入理解了红黑树的工作原理和优势所在。红黑树之所以在实践中比AVL树更具优势,关键在于其采用的"近似平衡"策略------不追求绝对的高度平衡,而是通过颜色规则确保最长路径不超过最短路径的两倍。这种设计使得红黑树在插入和删除操作时需要更少的旋转调整,从而在频繁修改的场景下展现出更好的性能。从实现细节来看,红黑树通过巧妙的双红缺陷处理机制,结合变色和旋转操作,既保证了操作的对数时间复杂度,又降低了维护平衡的开销,这正是它能够成为工业级标准平衡树的核心原因。


✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观 !

🚀 个人主页 :不呆头 · CSDN

🌱 代码仓库 :不呆头 · Gitee

📌 专栏系列 :

💬 座右铭 : "不患无位,患所以立。"

相关推荐
ShineWinsu20 分钟前
对于Qt:QPushButton常见属性的解析
开发语言·c++·qt·api·gui·按钮·qpushbutton
外贸GEO优化21 分钟前
外贸独立站的可抓取性:JS 渲染、懒加载与无限滚动到底影响什么
开发语言·javascript·ecmascript
飞浪纪元[FWC–FE]21 分钟前
Qt 工程创建与 CMake / qmake 构建规范
开发语言·数据库·qt
Lhan.zzZ30 分钟前
用 Qt 构建线程安全的通讯静态库:从线程亲和性到完整工程
c++·vscode·qt·tcp/ip·udp
后台模板学习33 分钟前
Go Web开发中任务调度框架的源码解析与高可用实现方案
开发语言·后端·golang
xiaoye-duck1 小时前
《C++ 项目:手写大模型接入 SDK》DeepSeek接口设计(上):初始化和全量响应
开发语言·网络·c++·语言模型
fpcc1 小时前
跟我学c++中级篇—streambuf在glog中的应用分析
开发语言·c++
吾与谁归in1 小时前
C#脚本工具,支持智能提示自动补全(WPF + Roslyn + AvalonEdit)
开发语言·c#·wpf
码匠许师傅1 小时前
【C++三方组件】OpenCV:计算机视觉的事实标准
c++