【数据结构】红黑树

目录

[1. 基本定义](#1. 基本定义)

[2. 核心特性](#2. 核心特性)

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

[4. 自平衡机制(变色与旋转)](#4. 自平衡机制(变色与旋转))

[5. 红黑树的插入](#5. 红黑树的插入)

[6. 红黑树的查找](#6. 红黑树的查找)

[7. 获取树的高度](#7. 获取树的高度)

[8. 红黑树的验证](#8. 红黑树的验证)

[8.1 BST性质验证](#8.1 BST性质验证)

[8.2 红黑树规则检查](#8.2 红黑树规则检查)

[9. 总结](#9. 总结)


1. 基本定义

红黑树是一种自平衡的二叉搜索树,它通过颜色规则和有限的旋转操作维持 "黑高度平衡",在插入 / 删除时的性能优于 AVL 树,是工程中应用最广泛的平衡树之一(如 C++ STL 的map/set、Java TreeMap的等底层实现)。

2. 核心特性

红黑树的每个节点除了存储键值、左右子节点和父节点外,还增加了颜色属性 (红色或黑色),并通过以下五条严格规则保证树的 "近似平衡":

  1. 每个节点要么是红色,要么是黑色;
  2. 根节点必须是黑色;
  3. 所有NIL 叶子节点(空节点)视为黑色;
  4. 红色节点的左右孩子必须是黑色的(即不存在两个连续的红色节点)。
  5. 从任一节点到NIL的所有路径黑色节点的数量相同

可简记为"根叶黑、不红红、黑路同"。
红黑树不追求 AVL 树那样的 "严格高度平衡"(左右子树高度差≤1),而是通过 "黑路同" 保证最长路径不超过最短路径的 2 倍,间接控制树的高度。这种 "宽松平衡" 使得红黑树在插入 / 删除时需要的旋转操作更少(最多 2 次旋转),效率更高。
红黑树插入节点默认选择红色而不是黑色,核心原因是:插入红色节点会红黑树规则的破坏更小,修复成本更低。

如果插入黑色节点,那么会破坏"黑路同"规则 ,所有经过该节点的路径会多一个黑色节点,这种破坏是全局性的 ,想要调整,相当于要重新平衡所有路径的黑节点数量,修复逻辑极其复杂。如果插入红色节点,对规则的破坏是局部且轻微的仅可能破坏"根叶黑""不红红"规则,通过局部的变色或旋转即可修复,不会涉及全局路径的调整,成本远低于修复"黑路同"。

3. 红黑树的节点定义

复制代码
// 枚举值表示颜色
enum Colour { RED, BLACK };

// 这里我们按key/value结构实现
template<class K, class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col; //节点颜色

	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_col(RED)  //新节点默认红色
	{}
};

4. 自平衡机制(变色与旋转)

插入节点的平衡调整

  • 若插入节点是根节点,破坏**"根叶黑"**规则,直接将其设为黑色就可以了。
  • 插入后,父节点是黑色的,规则未被破坏,无需调整。
  • 插入后,父节点是红色的,破坏**"不红红"** 规则,需要根据**"叔父节点的颜色"**分情况处理:

**情况 1:**叔父节点是红色 → 父节点和叔父节点变黑,祖父节点变红(向上递归检查);

**情况 2:**叔父节点是黑色 → 结合节点的位置(左 / 右孩子)执行旋转 + 变色( LL型、RR型、LR 型、RL型)。

情况1和情况2如图示:

示例:插入节点25流程

示例:插入节点5流程

5. 红黑树的插入

复制代码
bool Insert(const pair<K, V>& kv)
{
	//空树直接插入根节点(黑色)
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;

		return true;
	}

	//BST插入逻辑
	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;
		if (parent == grandfather->_left)//父亲节点是爷爷节点的左孩子
		{
			//     g
			//   p   u
			Node* uncle = grandfather->_right;

			//情况1:叔父节点为红色-->仅对叔父爷变色,并向上递归检查
			if (uncle && uncle->_col == RED) 
			{
				//对叔父爷变色
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				//祖父作为新节点继续检查
				cur = grandfather;
				parent = cur->_parent;
			}
			else // 情况2:叔父节点为黑色  旋转+变色
			{
				if (cur == parent->_left) //cur是父亲节点的左孩子  LL直线型
				{						
					RightRotate(grandfather); //右单旋

					//变色
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else  //cur是父亲节点的右孩子 LR折线型
				{						
					LeftRotate(parent);//左单旋
					RightRotate(grandfather);//右单旋

					//变色
					cur->_col = BLACK;
					grandfather->_col = RED;
				}

				break; //修复完成,无需继续循环
			}
		}
		else  //父亲节点是爷爷节点的右孩子 
		{
			//     g
			//   u   p
			Node* uncle = grandfather->_left;

            //情况1:叔父节点为红色-->仅对叔父爷变色,并向上递归检查
			if (uncle && uncle->_col == RED)  
			{
				//变色
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				// 祖父作为新节点继续检查
				cur = grandfather;
				parent = cur->_parent;
			}
			else //情况2:叔父节点为黑色  旋转+变色
			{
				if (cur == parent->_right)//cur为父亲节点的右孩子  RR直线型
				{
					LeftRotate(grandfather);//左单旋

					//变色
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else //cur为父亲节点的左孩子  RL折线型
				{
					RightRotate(parent);//右单旋
					LeftRotate(grandfather);//左单旋

					//变色
					cur->_col = BLACK;
					grandfather->_col = RED;
				}

				break; //修复完成,无需继续循环
			}
		}
	}

	_root->_col = BLACK; //确保根节点始终是黑色
	return true;
}

删除节点的平衡调整,删除操作更复杂(需维护黑高度一致),本文不作研究。

6. 红黑树的查找

复制代码
//查找
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key > cur->_kv.first)
			cur = cur->_right;
		else if (key < cur->_kv.first)
			cur = cur->_left;
		else
			return cur;
	}

	return nullptr;
}

7. 获取树的高度

复制代码
int _Height(Node* root)
{
	if (root == nullptr)
		return 0;

	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);

	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

8. 红黑树的验证

8.1 BST性质验证

通过中序遍历检查是否升序

复制代码
void _InOrder(Node* root)
{
	if (root == nullptr)
	{
		return;
	}
 
	_InOrder(root->_left);
	cout << root->_kv.first << " ";
	_InOrder(root->_right);
}

8.2 红黑树规则检查

验证红黑树的规则

  • **节点颜色只能是红 / 黑:**靠类型约束天然保证,无需检查;
  • **根节点为黑色:**直接检查根节点颜色,若根非空且颜色不是黑色则违规;
  • **不存在连续红色节点:**递归遍历每个节点,若当前节点为红色,则检查其父节点颜色(父节点必存在,除根节点外),若父节点也为红色,直接判定违规。
  • **所有路径黑节点数一致:**先确定 "参考黑节点数":选任意一条从根到空节点的路径(如一直向左),统计黑节点数量作为refNum;递归遍历所有路径:用形参blackNum记录 "从根到当前节点的黑节点数",遇到黑色节点则blackNum++;路径结束验证:走到空节点时,对比当前路径的blackNum和refNum,不一致则违规。
复制代码
bool IsBalanceTree()
{
	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);
}

bool Check(Node* root, int blackNum, const int refNum)
{
	if (root == nullptr)
	{
		//前序遍历走到空时,一条路径就走完了
		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);
}

9. 总结

红黑树比AVL数更常用的原因:

调整触发频率低(平衡条件宽松):

插入操作:红黑树约 50% 的情况仅需改变颜色,无需旋转;最坏情况仅需 2 次旋转,虽然AVL树的插入也最坏旋转两次,但调整的"成本结构"和"触发频率"差异极大;

删除操作:红黑树处理"黑高缺失"最多需要2次旋转,AVL树删除后可能引发"失衡传播",极端情况下需O(log n)次旋转。

查询性能差距可忽略:实际场景中 "足够快"

红黑树的高度虽然略高,但log₂n 和2log₂n在实际数量下(如n=100万时,log₂n≈20,2log₂n≈40),而现代 CPU 的指令执行速度(每秒数十亿次)下,对查找性能影响可忽略(微秒级甚至纳秒级差异)。

空间开销极小:适配底层系统与海量数据

红黑树:仅需为每个节点存储"颜色"标记------1位二进制即可(如0表示黑色,1表示红色),几乎不占用额外空间。AVL树:每个节点存储 "高度" 或 "平衡因子"------ 通常占用 4 字节(int 类型),若节点数量达千万级,AVL 树额外占用的空间会达到数十 MB,而红黑树可忽略不计。

对比维度 红黑树 AVL 树
平衡标准 黑高度一致(近似平衡):任意节点到叶子的路径中,黑色节点数相同;最长路径 ≤ 最短路径 ×2 严格高度平衡:任意节点的左右子树高度差 ≤ 1
树高上限 2log2​(n+1)(n 为节点数) log2​n+1(更矮)
插入旋转次数 最多 2 次(仅叔父为黑色时需要) 最多 2 次(单旋 / 双旋)
删除旋转次数 最多 2 次(修复双重黑色节点) 最多O(logn)次(可能连锁调整)
查找性能 略差(树高更高) 略优(树高更矮)
调整成本 低(优先变色,旋转少) 高(依赖旋转,可能连锁调整)
适用场景 频繁插入 / 删除(如容器:C++ map、Java TreeMap 频繁查找(如数据库索引、缓存)

模拟实现红黑树:

复制代码
// 枚举值表示颜色
enum Colour { RED, BLACK };

// 这里我们按key/value结构实现
template<class K, class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col; //节点颜色

	RBTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_col(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->_col = BLACK;

			return true;
		}

		//BST插入逻辑
		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;
			if (parent == grandfather->_left)//父亲节点是爷爷节点的左孩子
			{
				//     g
				//   p   u
				Node* uncle = grandfather->_right;

				//情况1:叔父节点为红色-->仅对叔父爷变色,并向上递归检查
				if (uncle && uncle->_col == RED) 
				{
					//对叔父爷变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					//祖父作为新节点继续检查
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况2:叔父节点为黑色  旋转+变色
				{
					if (cur == parent->_left) //cur是父亲节点的左孩子  LL直线型
					{						
						RightRotate(grandfather); //右单旋

						//变色
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else  //cur是父亲节点的右孩子 LR折线型
					{						
						LeftRotate(parent);//左单旋
						RightRotate(grandfather);//右单旋

						//变色
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break; //修复完成,无需继续循环
				}
			}
			else  //父亲节点是爷爷节点的右孩子 
			{
				//     g
				//   u   p
				Node* uncle = grandfather->_left;

				//情况1:叔父节点为红色-->仅对叔父爷变色,并向上递归检查
				if (uncle && uncle->_col == RED)  
				{
					//变色
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 祖父作为新节点继续检查
					cur = grandfather;
					parent = cur->_parent;
				}
				else //情况2:叔父节点为黑色  旋转+变色
				{
					if (cur == parent->_right)//cur为父亲节点的右孩子  RR直线型
					{
						LeftRotate(grandfather);//左单旋

						//变色
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else //cur为父亲节点的左孩子  RL折线型
					{
						RightRotate(parent); //右单旋
						LeftRotate(grandfather); //左单旋

						//变色
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

					break; //修复完成,无需继续循环
				}
			}
		}

		_root->_col = BLACK; //确保根节点始终是黑色
		return true;
	}

	//左单旋
	void LeftRotate(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		Node* parentparent = parent->_parent;

		subR->_left = parent;
		parent->_parent = subR;

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

	//右单旋
	void RightRotate(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;

		Node* parentparent = parent->_parent;

		subL->_right = parent;
		parent->_parent = subL;

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

	//中序遍历
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	//求树的高度
	int Height()
	{
		return _Height(_root);
	}

	size_t Size()
	{
		return _Size(_root);
	}

	//查找
	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (key > cur->_kv.first)
				cur = cur->_right;
			else if (key < cur->_kv.first)
				cur = cur->_left;
			else
				return cur;
		}

		return nullptr;
	}

	//验证红黑树
	bool IsBalanceTree()
	{
		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:

	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);

		return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	}

	size_t _Size(Node* root)
	{
		if (root == nullptr)
			return 0;

		return _Size(root->_left) + _Size(root->_right) + 1;
	}


	bool Check(Node* root, int blackNum, const int refNum)
	{
		if (root == nullptr)
		{
			//前序遍历走到空时,一条路径就走完了
			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);
	}

private:
	Node* _root = nullptr;
};
相关推荐
保持低旋律节奏2 小时前
算法——冗余!哈希表、vector、string适配器的混合使用
数据结构·算法·散列表
帅中的小灰灰2 小时前
C++编程建造器设计模式
java·c++·设计模式
喵个咪3 小时前
Qt 6 实战:C++ 调用 QML 回调方法(异步场景完整实现)
前端·c++·qt
立志成为大牛的小牛4 小时前
数据结构——五十一、散列表的基本概念(王道408)
开发语言·数据结构·学习·程序人生·算法·散列表
杨福瑞4 小时前
数据结构:双向链表(3)
c语言·数据结构·链表
立志成为大牛的小牛5 小时前
数据结构——五十二、散列函数的构造(王道408)
数据结构·笔记·程序人生·考研·算法
AAA阿giao6 小时前
大厂面试之反转字符串:深入解析与实战演练
前端·javascript·数据结构·面试·职场和发展·编程技巧
希望有朝一日能如愿以偿6 小时前
力扣每日一题:可被三整除的最大和
数据结构·算法·leetcode
阿波茨的鹅6 小时前
VSCode C++ 项目配置教程
c++·ide·vscode