了解红黑树并将其模拟实现
- 红黑树的概念和性质
- 1. 概念
- 2. 性质
- 红黑树的结构
- 红黑树的节点定义及红黑树结构成员定义
- 红黑树的插入
- 1. 按照二叉搜索的树规则插入新节点
- 2. 检测新节点插入后,红黑树的性质是否造到破坏
- 情况一: cur为红,p为红,g为黑,u存在且为红
- 情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
- 情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑
- 3. 插入代码解析
- 4. 插入动态效果
- 1. 以升序插入构建红黑树
- 2. 以降序插入构建红黑树
- 3. 随机插入构建红黑树
- 红黑树的验证
- 红黑树的遍历输出和左旋右旋
- 1. 遍历输出
- 2. 左单旋
- 3. 右单旋
- 红黑树模拟实现全部代码
- 红黑树的应用
- 红黑树与AVL树的比较
红黑树的概念和性质
1. 概念
红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red
或Black
。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路径会比其他路径长出俩倍,因而是接近平衡的
2. 性质
- 每个节点要么是红色,要么是黑色。
- 根节点是黑色的。
- 如果一个节点是红色的,那么它的两个子节点都是黑色的。
- 从任意节点到其每个叶子节点的每条路径都包含相同数量的黑色节点,这被称为“黑高相等性”。
- 每个叶子节点(NIL节点,通常表示为空节点)是黑色的。
红黑树的这些性质确保了树的平衡性,从而保证了树的高度在对数范围内,使得基本操作的时间复杂度保持在O(log n)
级别
红黑树的结构
为了后续实现关联式容器简单,红黑树的实现中增加一个头结点,因为跟节点必须为黑色,为了与根节点进行区分,将头结点给成黑色,并且让头结点的 pParent
域指向红黑树的根节点,pLeft
域指向红黑树中最小的节点,_pRight
域指向红黑树中最大的节点。
当在红黑树的实现中增加一个头结点时,目的是为了简化操作和处理边界情况,而不必为特殊情况单独编写代码。这个头结点通常是一个黑色节点,位于红黑树的根节点之上,其pParent
指向红黑树的根节点,pLeft
指向红黑树中最小的节点,pRight
指向红黑树中最大的节点。
以下是对头结点的作用和实现细节的解释:
- 简化边界条件处理:头结点的存在使得根节点始终是黑色的,因为根节点是头结点的右子节点。这样,在插入和删除等操作中,不需要特殊情况处理根节点的颜色和父节点是否存在的问题。
- 快速访问最小和最大节点:头结点的
pLeft
指向红黑树中最小的节点,pRight
指向最大的节点。这意味着你可以在O(1)时间内找到树中的最小和最大节点,而无需进行遍历。 - 统一根节点访问:无论是插入、删除还是其他操作,都可以始终从头结点开始进行操作,因为头结点的
pParent
指向了真正的根节点。这简化了代码逻辑,因为你不必特殊处理根节点的情况。
下面是一个头结点的示例实现:
struct Node {int data;Color color;Node* left;Node* right;Node* parent;
};class RedBlackTree {
private:Node* root; // 实际的根节点Node* header; // 头结点// ...其他成员函数和辅助函数...public:RedBlackTree() : root(nullptr), header(new Node) {header->color = BLACK;header->left = nullptr;header->right = nullptr;header->parent = nullptr;}// ...其他公共成员函数...
};
在这个示例中,我们添加了一个名为header
的成员变量,它是一个指向头结点的指针。头结点的初始化在构造函数中完成,并确保它始终是黑色的,根节点则位于头结点的pParent
中。头结点的存在将简化红黑树的操作和边界情况处理。
这是一种实现方式,但在后面我们实现不是采用的这种方式,当然你也可以选择这种方式实现,更为简易,红黑树的实现过程只是为了让我们更熟悉它的底层,现实中需要我们实现的场景很少。
红黑树的节点定义及红黑树结构成员定义
这里我们的实现采用了三叉链的形式,后面我们会提到这种写法的好处
我们这里使用键值对的模板是为了方便后期模拟实现map和set
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(kv){}
};template<class K, class V>
struct RBTree
{typedef RBTreeNode<K,V> Node;
private:Node* _root = nullptr;
};
-
枚举类型
Colour
:这里定义了一个枚举类型,包括两个成员RED
和BLACK
。这个枚举类型用于表示红黑树中节点的颜色。在红黑树中,节点可以是红色或黑色,用这个枚举类型来表示节点颜色是一种常见的做法。 -
结构体
RBTreeNode<K, V>
:这个结构体定义了红黑树的节点结构,其中包含以下成员变量:_left
和_right
:分别指向节点的左子节点和右子节点的指针。_parent
:指向节点的父节点的指针。_kv
:用于存储键值对(Key-Value Pair)的成员变量。这个键值对通常用于存储节点的数据。_col
:表示节点的颜色,可以是RED
或BLACK
。
构造函数
RBTreeNode(const pair<K, V>& kv)
用于初始化节点对象,将各个成员变量赋初值,包括键值对_kv
和颜色_col
。 -
结构体
RBTree<K, V>
:这个结构体定义了整个红黑树的结构,其中包含以下成员变量和一个别名:_root
:指向红黑树的根节点的指针。根节点是红黑树的起始点,所有操作都从根节点开始。
typedef RBTreeNode<K,V> Node;
定义了一个别名Node
,用于表示红黑树节点的类型。这样做可以简化代码,使得在代码中引用节点类型更加方便。
红黑树的插入
1. 按照二叉搜索的树规则插入新节点
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)//同搜索树规则找到插入位置{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* grandfater = parent->_parent;assert(grandfater);assert(grandfater->_col == BLACK);if (parent == grandfater->_left)//具体看下面讲解{Node* uncle = grandfater->_right;// 情况一 : uncle存在且为红,变色+继续往上处理if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfater->_col = RED;// 继续往上处理cur = grandfater;parent = cur->_parent;}// 情况二+三:uncle不存在 + 存在且为黑else{// 情况二:右单旋+变色if (cur == parent->_left){RotateR(grandfater);parent->_col = BLACK;grandfater->_col = RED;}else{// 情况三:左右单旋+变色RotateL(parent);RotateR(grandfater);cur->_col = BLACK;grandfater->_col = RED;}break;}}else // (parent == grandfater->_right){Node* uncle = grandfater->_left;// 情况一if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfater->_col = RED;// 继续往上处理cur = grandfater;parent = cur->_parent;}else{// 情况二:左单旋+变色if (cur == parent->_right){RotateL(grandfater);parent->_col = BLACK;grandfater->_col = RED;}else{// 情况三:右左单旋+变色RotateR(parent);RotateL(grandfater);cur->_col = BLACK;grandfater->_col = RED;}break;}}}_root->_col = BLACK;return true;
}
其实前面的代码和我们上一篇讲到的AVL树的插入类似,只不过这里的红黑树没有了平衡因子,而变为了颜色
接下来我们看每种情况的解析
2. 检测新节点插入后,红黑树的性质是否造到破坏
因为新节点的默认颜色是红色,因此:如果其双亲节点的颜色是黑色,没有违反红黑树任何性质,则不需要调整;但当新插入节点的双亲节点颜色为红色时,就违反了性质三不能有连在一起的红色节点,此时需要对红黑树分情况来讨论:
约定:cur为当前节点,p为父节点,g为祖父节点,u为叔叔节点
情况一: cur为红,p为红,g为黑,u存在且为红
cur和p均为红解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整
情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑
p为g的左孩子,cur为p的左孩子,则进行右单旋转;相反,p为g的右孩子,cur为p的右孩子,则进行左单旋转,p、g变色–p变黑,g变红
情况三: cur为红,p为红,g为黑,u不存在/u存在且为黑
p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;相反,p为g的右孩子,cur为p的左孩子,则针对p做右单旋转,则转换成了情况2
3. 插入代码解析
- 如果
_root
是空的,表示红黑树为空,那么将创建一个新的节点_root
,并将其颜色设置为黑色。这是根节点,并且遵循红黑树规则的规定,根节点必须为黑色。 - 如果
_root
不为空,那么代码会在树中查找正确的位置来插入新节点。通过遍历树的方式,找到正确的父节点parent
,以及新节点要插入的位置cur
。 - 如果要插入的键已经存在于树中,就返回
false
,因为不允许键重复。 - 创建新节点
cur
并将其颜色设置为红色。新节点的颜色一开始设置为红色,这是为了满足红黑树的性质。 - 然后,代码进入一个循环,循环的目的是确保插入新节点后仍然满足红黑树的性质。这是红黑树插入操作的关键部分。
- 在循环中,首先查看
parent
节点的颜色,如果parent
是红色,那么需要进一步处理来保持红黑树性质。 - 然后,代码会检查
uncle
节点的颜色,即parent
的兄弟节点。根据uncle
的颜色,分为情况一、情况二和情况三。 - 最后,退出循环后将根节点的颜色设置为黑色,以确保根节点满足红黑树的性质。
- 在循环中,首先查看
4. 插入动态效果
1. 以升序插入构建红黑树
2. 以降序插入构建红黑树
3. 随机插入构建红黑树
红黑树的验证
bool IsBalance(){if (_root == nullptr){return true;}if (_root->_col == RED)//检查根节点是否为黑{cout << "根节点不是黑色" << endl;return false;}// 黑色节点数量基准值int benchmark = 0;return PrevCheck(_root, 0, benchmark);}
返回值函数PrevCheck的实现
bool PrevCheck(Node* root, int blackNum, int& benchmark)
{if (root == nullptr){if (benchmark == 0){benchmark = blackNum;return true;}if (blackNum != benchmark){cout << "某条黑色节点的数量不相等" << endl;return false;}else{return true;}}if (root->_col == BLACK){++blackNum;}if (root->_col == RED && root->_parent->_col == RED){cout << "存在连续的红色节点" << endl;return false;}return PrevCheck(root->_left, blackNum, benchmark)&& PrevCheck(root->_right, blackNum, benchmark);
}
- 黑高度相同性质检查:函数使用递归方式遍历树中的节点,并维护一个
blackNum
变量,用于跟踪从根节点到当前节点经过的黑色节点数。在遍历过程中,它会检查每条从根到叶子节点的路径上的黑色节点数是否相同。如果路径上的黑色节点数不相同,说明违反了红黑树的性质。 - 连续红色节点检查:在遍历的过程中,代码还检查是否存在连续的红色节点。在红黑树中,不允许存在相邻的两个红色节点。如果存在连续的红色节点,也违反了红黑树的性质。
- 返回值:函数返回一个布尔值,用于指示红黑树是否满足性质。如果在遍历过程中发现任何性质被破坏,函数将返回
false
,否则返回true
。
这个函数是用于验证红黑树的一种常见方法,用于检查树是否保持了红黑树的性质,包括黑高度相同和不连续的红色节点。如果该函数返回 true
,则表示树是一个有效的红黑树,否则表示树的结构违反了红黑树的性质。
红黑树的遍历输出和左旋右旋
1. 遍历输出
void _InOrder(Node* root)
{if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);
}
这里的遍历是一种按照节点值的大小顺序遍历二叉搜索树(BST)的方法。在这里,红黑树也是一种BST,因此中序遍历可以用来按键的顺序输出树中的节点。
函数的主要逻辑包括:
- 如果输入的
root
节点为空(即树为空),则直接返回,不执行任何操作。 - 否则,函数会递归地执行以下操作:
- 先递归调用
_InOrder
函数来遍历左子树(左子节点)。 - 输出当前节点的键值对(这里假设键是整数,值是整数,根据需要调整输出格式)。
- 最后递归调用
_InOrder
函数来遍历右子树(右子节点)。
- 先递归调用
2. 左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node* ppNode = parent->_parent;subR->_left = parent;parent->_parent = subR;if (_root == parent){_root = subR;subR->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_parent = ppNode;}}
- 首先,将
parent
节点的右子节点subR
和subR
的左子节点subRL
分别保存起来。subR
将会成为新的根节点,而subRL
将成为parent
节点的右子节点。 - 接下来,将
parent
节点的右子节点指针_right
指向subRL
,同时确保subRL
的父指针_parent
指向parent
。这一步是将subRL
与parent
连接起来。 - 将
parent
节点的父节点指针ppNode
保存起来。这是为了在旋转后更新ppNode
的左子节点或右子节点指向subR
,以确保树的连接正确。 - 将
subR
的左子节点指针_left
指向parent
,同时将parent
的父节点指针_parent
指向subR
。这一步是将parent
旋转到subR
的位置上。 - 接下来,处理根节点的情况。如果
_root
指向parent
,那么现在_root
应该指向subR
,因此将_root
更新为subR
,并将subR
的父指针设置为nullptr
。 - 如果
_root
不指向parent
,则需要根据ppNode
的情况来更新ppNode
的左子节点或右子节点指向subR
,以确保整棵树的连接正确。
左单旋和右单旋和我们之前的AVL树实现基本一致,若不能理解请看上一篇文章
3. 右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR){subLR->_parent = parent;}Node* ppNode = parent->_parent;subL->_right = parent;parent->_parent = subL;if (_root == parent){_root = subL;subL->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_parent = ppNode;}}
- 首先,将
parent
节点的左子节点subL
和subL
的右子节点subLR
分别保存起来。subL
将会成为新的根节点,而subLR
将成为parent
节点的左子节点。 - 接下来,将
parent
节点的左子节点指针_left
指向subLR
,同时确保subLR
的父指针_parent
指向parent
。这一步是将subLR
与parent
连接起来。 - 将
parent
节点的父节点指针ppNode
保存起来。这是为了在旋转后更新ppNode
的左子节点或右子节点指向subL
,以确保树的连接正确。 - 将
subL
的右子节点指针_right
指向parent
,同时将parent
的父节点指针_parent
指向subL
。这一步是将parent
旋转到subL
的位置上。 - 接下来,处理根节点的情况。如果
_root
指向parent
,那么现在_root
应该指向subL
,因此将_root
更新为subL
,并将subL
的父指针设置为nullptr
。 - 如果
_root
不指向parent
,则需要根据ppNode
的情况来更新ppNode
的左子节点或右子节点指向subL
,以确保整棵树的连接正确。
红黑树模拟实现全部代码
#pragma once
#include<iostream>
#include<assert.h>
#include<time.h>
using namespace std;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(kv){}
};template<class K, class V>
struct 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;}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);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* grandfater = parent->_parent;assert(grandfater);assert(grandfater->_col == BLACK);if (parent == grandfater->_left){Node* uncle = grandfater->_right;// 情况一 : uncle存在且为红,变色+继续往上处理if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfater->_col = RED;// 继续往上处理cur = grandfater;parent = cur->_parent;}// 情况二+三:uncle不存在 + 存在且为黑else{// 情况二:右单旋+变色if (cur == parent->_left){RotateR(grandfater);parent->_col = BLACK;grandfater->_col = RED;}else{// 情况三:左右单旋+变色RotateL(parent);RotateR(grandfater);cur->_col = BLACK;grandfater->_col = RED;}break;}}else // (parent == grandfater->_right){Node* uncle = grandfater->_left;// 情况一if (uncle && uncle->_col == RED){parent->_col = uncle->_col = BLACK;grandfater->_col = RED;// 继续往上处理cur = grandfater;parent = cur->_parent;}else{// 情况二:左单旋+变色if (cur == parent->_right){RotateL(grandfater);parent->_col = BLACK;grandfater->_col = RED;}else{// 情况三:右左单旋+变色RotateR(parent);RotateL(grandfater);cur->_col = BLACK;grandfater->_col = RED;}break;}}}_root->_col = BLACK;return true;}void InOrder(){_InOrder(_root);cout << endl;}bool IsBalance(){if (_root == nullptr){return true;}if (_root->_col == RED){cout << "根节点不是黑色" << endl;return false;}// 黑色节点数量基准值int benchmark = 0;return PrevCheck(_root, 0, benchmark);}private:bool PrevCheck(Node* root, int blackNum, int& benchmark){if (root == nullptr){if (benchmark == 0){benchmark = blackNum;return true;}if (blackNum != benchmark){cout << "某条黑色节点的数量不相等" << endl;return false;}else{return true;}}if (root->_col == BLACK){++blackNum;}if (root->_col == RED && root->_parent->_col == RED){cout << "存在连续的红色节点" << endl;return false;}return PrevCheck(root->_left, blackNum, benchmark)&& PrevCheck(root->_right, blackNum, benchmark);}void _InOrder(Node* root){if (root == nullptr){return;}_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << endl;_InOrder(root->_right);}void RotateL(Node* parent){Node* subR = parent->_right;Node* subRL = subR->_left;parent->_right = subRL;if (subRL)subRL->_parent = parent;Node* ppNode = parent->_parent;subR->_left = parent;parent->_parent = subR;if (_root == parent){_root = subR;subR->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_parent = ppNode;}}void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR){subLR->_parent = parent;}Node* ppNode = parent->_parent;subL->_right = parent;parent->_parent = subL;if (_root == parent){_root = subL;subL->_parent = nullptr;}else{if (ppNode->_left == parent){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_parent = ppNode;}}private:Node* _root = nullptr;
};
红黑树的应用
红黑树是一种自平衡的二叉搜索树,由于其平衡性和高效性能,广泛用于各种计算机科学和软件工程领域。以下是一些红黑树的常见应用:
- C++ STL中的
std::map
和std::set
:C++标准模板库(STL)中的std::map
和std::set
通常使用红黑树实现。它们用于实现有序的关联容器,其中键值对被自动排序并保持平衡,以支持高效的查找、插入和删除操作。 - 数据库系统:红黑树常用于数据库系统中的索引结构,如B+树的实现中。数据库中的索引需要高效地支持查找、范围查询和排序等操作,而红黑树是一种性能良好的选择。
- 操作系统:红黑树在操作系统中用于进程调度、虚拟内存管理和文件系统的实现。在这些场景中,需要高效的数据结构来管理和操作各种系统资源。
- 网络路由表:红黑树被广泛用于路由表的实现,以支持网络路由器和交换机等网络设备的高效路由查找。
- 编译器:编译器可以使用红黑树来管理符号表,以便进行语法分析、类型检查和代码生成。
- 计算机图形学:在计算机图形学中,红黑树可用于空间分割数据结构,如八叉树和四叉树的实现,以支持高效的图形渲染和碰撞检测。
- 高性能库和框架:红黑树常被用作高性能库和框架中的核心数据结构,以提供高效的数据管理和操作功能。
- 实时系统:实时系统需要高效的数据结构来管理任务和事件,红黑树可以用于实现定时器和调度器。
总的来说,红黑树是一种多功能的数据结构,适用于需要高效的自平衡二叉搜索树的任何领域,特别是需要高效的插入、删除和查找操作的场景。其平衡性和性能使其成为各种应用中的重要工具。
红黑树与AVL树的比较
红黑树(Red-Black Tree)和AVL树(Adelson-Velsky and Landis Tree)都是自平衡二叉搜索树,它们在维护平衡性和支持高效的插入、删除和查找操作方面有很多相似之处,但也有一些关键的区别。以下是红黑树和AVL树之间的比较:
- 平衡性要求:
- 红黑树:红黑树放宽了平衡性要求,它保证了树的高度最多是其2倍。
- AVL树:AVL树要求更为严格,它保证树的高度差不超过1,因此AVL树的平衡性更强。
- 性能:
- 红黑树:由于平衡性要求相对较低,插入和删除操作在平均情况下可能比AVL树更快,因此在那些需要频繁插入和删除操作的场景中,红黑树可能更适合。
- AVL树:AVL树在查找操作上可能稍微快一些,因为它的平衡性更严格,但它的插入和删除操作可能更慢,因为它需要更频繁地执行旋转操作来保持平衡。
- 旋转操作:
- 红黑树:红黑树的旋转操作相对较少,因为它放宽了平衡性要求。通常,红黑树的旋转操作较少,但需要更多的颜色变换操作来保持平衡。
- AVL树:AVL树的旋转操作更频繁,因为它要求更严格的平衡性。这可能导致在插入和删除操作中执行更多的旋转。
- 内存消耗:
- 红黑树:由于红黑树的平衡性要求较低,通常会消耗更少的内存,因为不需要额外的平衡因子来跟踪节点的高度差。
- AVL树:AVL树需要为每个节点存储平衡因子,这可能会导致更多的内存消耗。
- 应用场景:
- 红黑树:适用于需要高效的插入和删除操作,而对查找操作的性能要求相对较低的场景,如C++的STL中的
std::map
和std::set
。 - AVL树:适用于对查找操作有更高性能要求的场景,但可以容忍较慢的插入和删除操作的情况,如数据库索引。
- 红黑树:适用于需要高效的插入和删除操作,而对查找操作的性能要求相对较低的场景,如C++的STL中的
总的来说,选择使用红黑树还是AVL树取决于应用的需求和性能要求,在C++的map和set的实现中,底层调用的就是红黑树,但在实际的面试和工作中,红黑树的应用可能更为广泛。