C++之红黑树

目录

[(一) 前言](#(一) 前言)

[(二) 正文](#(二) 正文)

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

[(2) 红黑树的模拟](#(2) 红黑树的模拟)

[2.1 红黑树的插入](#2.1 红黑树的插入)

[(3) 红黑树的验证](#(3) 红黑树的验证)

[(4) AVL树和红黑树的比较](#(4) AVL树和红黑树的比较)


(一) 前言

在上一篇文章中,我们学习了 AVL 树,本次将重点学习红黑树。在正式开始前,我们先通过对比两种树的核心特性,理解红黑树的应用价值。​

AVL 树是严格平衡的二叉搜索树,要求任意节点的左右子树高度差不超过 1;而红黑树是接近平衡的二叉搜索树,仅要求 "最长路径的节点数不超过最短路径的 2 倍"

从高度数据来看:​

  • 当数据量为 1000 时,AVL 树的高度约为 logN(≈10),红黑树的高度约为 2logN(≈20);
  • 当数据量达到 10 亿时,AVL 树的高度约为 30,红黑树的高度约为 60。

显然红黑树的高度更高,但实际应用中红黑树反而更优,原因是什么呢?

对 CPU 而言,"30 与 60""10 与 20" 的计算开销差异极小,二者性能处于同一量级;但 AVL 树的 "严格平衡" 是有代价的 ------ 插入和删除操作中,为了维持高度差限制,需要进行大量的旋转调整。相比之下,红黑树的平衡要求更宽松,旋转操作更少,整体效率更高。因此,深入学习红黑树具有重要的实用意义。


(二) 正文

(1) 红黑树的概念和性质

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

红黑树图像:

红黑树的性质(需注意路径要从根节点走到空节点)

  1. 每个结点不是红色就是黑色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则它的两个孩子结点必须是黑色的 (任何路径都不会存在连续的红色结点,但是可以存在连续的黑色节点)
  4. 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点
  5. 每个叶子结点都是黑色的(此处的叶子结点指的是空结点)(即所有的NIL叶子节点为黑色)

小思考:

为什么满足上面的性质,红黑树就能保证:其最长路径中节点个数不会超过最短路径节点 个数的两倍?(首先,最短路径的节点必然全为黑色)

  1. 设最短路径的黑色节点数为 n(即该路径的黑高为 n),根据性质 4,所有路径的黑高均为 n;
  2. 最长路径的结构必然是 "黑红交替" ------ 因为性质 3 禁止连续的红色节点,要让路径最长,需在黑色节点之间尽可能插入红色节点;
  3. 对于 "黑红交替" 的最长路径,黑色节点数为 n(与黑高一致),红色节点数最多也为 n(每两个黑色节点间插一个红色节点),因此最长路径的总节点数为 "黑色节点数 + 红色节点数"=n+n=2n;
  4. 最短路径的节点数为 n(全黑),最长路径节点数为 2n,因此 "最长路径节点数不超过最短路径的 2 倍"。

同时可推出:最长路径中黑色节点的占比≥1/2,进一步说明红黑树的结构始终接近平衡。


(2) 红黑树的模拟

依旧得先定义树和树的节点

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

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

	pair<K, V> _kv;
	Colour _col;

	RBTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, parent(nullptr)
		, _kv
		, _col(BLACK)
	{}

};

template<class K,class V>
class RBTree
{
	typedef RBTreeNode<k, V> Node;
public:
    RBTree()
	    :_root(nullptr)
    {}

    //内容
private:
	Node* _root;
};

小知识点:enum的介绍

enum的基本使用方法:

复制代码
enum 枚举名 {
    常量1,
    常量2,
    ...
};
  • 枚举名 是自定义的类型名(如你代码中的 Colour);
  • 大括号内是枚举常量(如 RED、BLACK),它们本质是整数,默认从 0 开始依次递增(RED 默认为 0,BLACK 默认为 1);

也可以手动指定枚举常量的值

复制代码
enum Colour {
    RED = 2,  // 手动指定为2
    BLACK = 5 // 手动指定为5
};

简单说,枚举就是给一组整数 "起名字",让代码更易懂、更规范。

2.1 红黑树的插入

新插入的节点为什么颜色,为什么了?

**红色节点,**因为将新节点设为红色能最小化对红黑树性质的破坏。红色节点不会改变路径的黑高,而插入黑色节点会直接增加该路径的黑节点数量,违反"所有路径黑节点数相同"的性质,并且会影响整棵树的所有路径,需要更多调整操作。相比之下,插入红色节点只会影响当前路径,所需的调整操作更少。

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

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

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

由于新节点默认设置为红色,会出现以下两种情况:

  1. 若其父节点为黑色,则不违反任何红黑树性质,无需调整;
  2. 若父节点为红色,则违反了"不允许相邻两个节点均为红色"的性质(性质三)。

面对二的情况需根据具体情况进行调整,具体如下:(先约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点)

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

操作步骤

  1. 将 p 和 u 改为黑色
  2. 将 g 改为红色
  3. 将 g 设为新的当前节点(cur),继续向上调整

注意:

  • 若 g 是根节点,调整后需将其重新改为黑色;
  • 若 g 不是根节点,则其必有父节点。若 g 的父节点为红色,则需继续向上调整。

具体图像:

抽象图片解释:

情况二: cur 为红, p 为红, g 为黑, u 不存在或为黑(且 cur 与 p 、 p 与 g 呈直线关系)

判断条件:

  • 若 p 是 g 的左孩子,且 cur 是 p 的左孩子(直线型),则为左直线情况。
  • 若 p 是 g 的右孩子,且 cur 是 p 的右孩子(直线型),则为右直线情况。

解决方法:

  • 左直线情况: 执行右单旋转
  • 右直线情况: 执行左单旋转
  • 旋转后,将 p 设为黑色,g 设为红色

关于 u 的两种情况说明:

  1. 若 u 节点不存在,则 cur 必定是新插入节点。因为如果 cur 不是新插入节点,根据性质4(每条路径黑色节点数量相同),cur 或 p 中至少有一个应为黑色节点。

具体图:

  1. 若 u 节点存在,则其颜色必为黑色。此时 cur 节点原本应为黑色,其呈现红色的原因是在子树调整过程中将 cur 节点的颜色由黑色改为红色。

具体图;

两种情况的抽象图

情况三: cur 为红, p 为红, g 为黑, u 不存在或为黑(且 cur 与 p 、 p 与 g 呈折线关系)

处理方式:

  1. 若 p 是 g 的左子节点且 cur 是 p 的右子节点(折线型),则先对 p 执行左旋操作,将其转换为情况二的左直线形态;
  2. 若 p 是 g 的右子节点且 cur 是 p 的左子节点(折线型),则先对 p 执行右旋操作,将其转换为情况二的右直线形态。

具体图:

抽象图:

具体代码:

复制代码
bool Insert(const pair<K, V>& kv)
{
	//按照二叉搜索的树规则插入新节点
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	
	while (cur != nullptr)
	{
		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);
	cur->_col = RED;

	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)
		{
			Node* uncle = grandfather->_right;
			//情况一: cur为红,p为红,g为黑,u存在且为红
			if (uncle != nullptr && uncle->_col == RED)
			{
				//将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			//情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
			else
			{
				if (cur == parent->_left)
				{
					RotateR(grandfather);
					parent->_col = BLACK;
					grandfather->_col = RED;
				}
				else
				{
					RotateL(parent);
					RotateR(grandfather);

					cur->_col = BLACK;
					grandfather->_col = RED;
				}

				break;
			}
		}
		else  //parent == grandfather->_right
		{
			Node* uncle = grandfather->_left;
			//情况一: cur为红,p为红,g为黑,u存在且为红
			if (uncle != nullptr && uncle->_col == RED)
			{
				//将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			//情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
			else
			{
				if (cur == parent->_right)
				{
					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;
}

(3) 红黑树的验证

红黑树的验证过程包含两个主要步骤:

  1. 检查是否符合二叉搜索树特性(通过中序遍历验证是否为有序序列)
  2. 验证是否满足红黑树的所有性质

我们的实现重点在于第二项的验证,主要包括以下三个方面:

  1. 根节点颜色必须为黑色
  2. 不能出现连续的红色节点
  3. 所有路径的黑色节点数量必须相同

具体实现详见以下代码:

复制代码
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);
}

bool IsBalance()
{
	return _IsBalance(_root);
}

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

	if (root->_col != BLACK)
	{
		cout << "根节点颜色不是黑色" << endl;
		return false;
	}

	int benchmark = 0;
	Node* cur = root;
	while (cur != nullptr)
	{
		if (cur->_col == BLACK)
			++benchmark;

		cur = cur->_left;
	}

	return CheckColour(root, 0, benchmark);
}

测试用例代码为:

复制代码
void test1()
{
	//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	RBTree<int, int> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
		cout << "Insert:" << e << "->" << t.IsBalance() << endl;
	}

}

void test2()
{
	const int N = 10000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));

	for (size_t i = 0; i < N; i++)
	{
		v.push_back(i);
	}

	RBTree<int, int> rbt;
	for (auto e : v)
	{
		rbt.Insert(make_pair(e, e));
		//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
	}
	cout << rbt.IsBalance() << endl;
	
}

运行结果:

复制代码
test1的
Insert:16->1
Insert:3->1
Insert:7->1
Insert:11->1
Insert:9->1
Insert:26->1
Insert:18->1
Insert:14->1
Insert:15->1

Insert:4->1
Insert:2->1
Insert:6->1
Insert:1->1
Insert:3->1
Insert:5->1
Insert:15->1
Insert:7->1
Insert:16->1
Insert:14->1


test2的
1

完整代码------博主的gitee


(4) AVL树和红黑树的比较

红黑树和AVL树都是高效的平衡二叉搜索树,均能保证查找、插入、删除等操作的时间复杂度为O(logN)。两者的主要区别在于平衡策略:红黑树通过放宽平衡条件,仅要求最长路径不超过最短路径的两倍,从而减少了插入和删除时的旋转操作次数。这种设计使红黑树在频繁修改的场景中性能优于AVL树。此外,红黑树的实现相对简单,因此在工程实践中应用更为广泛。

测试方法:

我们可以再AVLTree和RBTRee的代码里面再加以一个测量高度和旋转次数的代码

测量高度的代码:

复制代码
int Height()
{
    return Height(_root);
}

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

要实现旋转次数统计功能,可以在左旋和右旋操作中增加++_rotateCount计数语句,同时在类成员变量中声明一个公共的_rotateCount计数器。

最后测试案例变为

复制代码
void test3()
{
    const int N = 10000;
    vector<int> v;
    v.reserve(N);
    srand(time(0));

    for (size_t i = 0; i < N; i++)
    {
        v.push_back(i);
    }

    RBTree<int, int> rbt;
    for (auto e : v)
    {
        rbt.Insert(make_pair(e, e));
        //cout << "Insert:" << e << "->" << t.IsBalance() << endl;
    }
    cout << rbt.IsBalance() << endl;
    cout << rbt.Height() << endl;
    cout << rbt._rotateCount << endl;


    AVLTree<int, int> avlt;
    for (auto e : v)
    {
        avlt.Insert(make_pair(e, e));
        //cout << "Insert:" << e << "->" << t.IsBalance() << endl;
    }
    cout << avlt.IsAVLTree() << endl;
    cout << avlt.Height() << endl;
    cout << avlt._rotateCount << endl;


    return 0;
}

测试结果:

复制代码
1
24
9976

1
14
9986

由结果我们可以发现:

1. 平衡性质:均保高效,规避退化

  • AVL 树:严格保证 "节点左右子树高度差≤1"(绝对平衡);
  • 红黑树:通过 "根黑、无连续红节点" 等规则,保证 "最长路径≤2× 最短路径"(近似平衡);

二者均将增删改查时间复杂度稳定在O(logN),避免普通 BST 退化为链表(O (N))。
2. 树高:AVL 更优,红黑够用

  • AVL 树高 14:绝对平衡压缩树高,查询时比较次数少,理论查询性能更好;
  • 红黑树高 24:虽高于 AVL,但仍属 O (logN) 级别,以略高树高换增删时更少的旋转开销。

3. 旋转次数:红黑开销更低

  • AVL 树:需维持绝对平衡,高度失衡就旋转,易触发连锁旋转,开销高;
  • 红黑树:仅 "连续红节点" 时调整,优先变色(不旋转),仅变色无效时旋转(最多 2 次 / 调整),频率更低,印证其增删性能更优。

总结:适用场景分化

  • **AVL 树:**适合查询多、增删少的场景------ 绝对平衡最大化查询效率,增删开销可忽略;
  • **红黑树:**适合增删频繁的场景------ 近似平衡保查询效率,低旋转开销 + 易实现,工程应用更广。

以上就是C++之红黑树的学习的学习。希望这些知识能为你带来帮助!如果觉得内容实用,欢迎点赞支持~ 若发现任何问题或有改进建议,也请随时与我交流。感谢你的阅读!

相关推荐
亮剑20182 小时前
第2节:程序逻辑与控制流——让程序“思考”
开发语言·c++·人工智能
敲代码的瓦龙2 小时前
操作系统?进程!!!
linux·c++·操作系统
TiAmo zhang3 小时前
现代C++的AI革命:C++20/C++23核心特性解析与实战应用
c++·人工智能·c++20
z187461030033 小时前
list(带头双向循环链表)
数据结构·c++·链表
来荔枝一大筐4 小时前
C++ LeetCode 力扣刷题 541. 反转字符串 II
c++·算法·leetcode
报错小能手4 小时前
C++笔记——STL list
c++·笔记
T.Ree.4 小时前
cpp_list
开发语言·数据结构·c++·list
laocooon5238578864 小时前
C++ 图片加背景音乐的处理
开发语言·c++
apocelipes4 小时前
POSIX兼容系统上read和write系统调用的行为总结
linux·c语言·c++·python·golang·linux编程