红黑树
红黑树是一种高效的自平衡二叉查找树,通过特定规则和操作确保平衡,从而保证各项操作的时间复杂度为O(log n)。
红黑树因其在动态插入 / 删除操作与查询效率之间的平衡特性,被广泛应用于多种系统和框架中:
- C++ STL:
std::map
、std::set
基于红黑树实现。 - Java集合框架:
TreeMap
、TreeSet
使用红黑树。 - 数据库索引:部分数据库的索引结构采用红黑树或其变种。
- Linux内核:进程调度、虚拟内存管理等模块使用红黑树管理数据。
红黑树与 AVL 树的核心差异在于平衡严格性与动态操作效率的权衡,AVL 树通过严格平衡因子(任意节点左右子树高度差≤1)保证绝对平衡,查询效率略高,但插入 / 删除时可能需要 O (log n) 次旋转调整,导致较高的维护成本;红黑树则通过颜色规则(如红节点子节点必为黑)维持近似平衡,插入最多 2 次旋转、删除最多 3 次,更适应频繁更新的场景。因此,AVL 树适合查询密集型任务(如数据库索引),而红黑树在插入 / 删除频繁的系统中表现更优(如 STL 的 map、Java 的 TreeMap)。
红黑树满足以下五个核心性质:
- 根节点为黑色。
- 每个节点要么是红色,要么是黑色
- 叶子节点(NIL节点)均为黑色。这里的叶子结点是最后的null结点
- 红色节点的子节点必须为黑色(即不存在连续两个红色节点)。
- 从任意节点到其所有叶子节点的路径包含相同数量的黑色节点(称为“黑高”一致)。
这些性质保证了最长路径不会超过最段路径的两倍,最长路径是红黑相间的,最段路径是连续黑的
下面是红黑树的结点结构,相较于普通的二叉搜索树它多了颜色与父节点指针的属性,颜色很简单因为我们的红黑树,所有的结点非黑即红所以直接使用bool来表示就可以了,parent父结点的作用是为后续操作提供可能,后期会经常需要知道插入或删除结点的,叔叔结点或兄弟结点的颜色,我们需要通过父结点和爷爷结点来获得这些信息
struct BinaryTreeNode {
bool isRed{true};
int height{1};
int data;
BinaryTreeNode* left;
BinaryTreeNode* right;
BinaryTreeNode* parent;
BinaryTreeNode(int val, bool c = true)
: isRed(c),data(val),left(nullptr),right(nullptr),parent{nullptr} {}
};
插入
插入的基本操作与二叉搜索树一样,我们只需要在插入完成后,检查插入的结点是否符合一定规则。新插入的节点默认颜色为红色,这是因为将新节点设为红色更有可能不破坏红黑树的性质 5(黑色高度平衡),即使破坏了性质 4(不能有两个连续的红色节点),也相对容易调整。
插入新节点后,需要检查红黑树的性质是否被破坏。若破坏了性质 4(新节点的父节点是红色),就需要进行调整,调整过程根据不同情况分为以下两种情形:
- 叔叔节点是红色
- 条件:新节点
z
的父节点p
是红色,且z
的叔叔节点u
(父节点的兄弟节点)也是红色。 - 调整操作:
- 将父节点
p
、叔叔节点u
和祖父节点g
的颜色反转。 - 把当前节点
z
指向祖父节点g
,继续向上检查和调整。
- 将父节点
- 条件:新节点
- 叔叔节点是黑色
- 条件:新节点
z
的父节点p
是红色,叔叔节点u
是黑色,如果叔叔结点是null也认为是黑色 - 调整操作:
- 检测祖父结点的的平衡因子,进行相应的旋转,旋转完成后将旋转点与旋转中心点的颜色反转
下面是我们的检测函数
- 检测祖父结点的的平衡因子,进行相应的旋转,旋转完成后将旋转点与旋转中心点的颜色反转
- 条件:新节点
void BinarySearchTree::_fixInsertion(BinaryTreeNode *node) {
if (node == _rootNode){
node->isRed = false;
return;
}
BinaryTreeNode* father = node->parent;
if (!father->isRed) return;
BinaryTreeNode* grandfather = father->parent;
if (grandfather == nullptr) return;
BinaryTreeNode* uncle = father == grandfather->left ?
grandfather->right : grandfather->left;
if (uncle == nullptr || uncle->isRed == false){
if (grandfather == _rootNode) _rootNode = spinTree(grandfather);
else {
grandfather->parent->left == grandfather ?
grandfather->parent->left = spinTree(grandfather) :
grandfather->parent->right = spinTree(grandfather);
}
}else{
grandfather->isRed = !grandfather->isRed;
uncle->isRed = !uncle->isRed;
father->isRed = !father->isRed;
_fixInsertion(grandfather);
}
}
这个函数一开始会检测传入的结点是不是跟结点,如果是就将其颜色改为黑色并退出,如果不是后续会获得当前结点的,父结点祖父结点叔叔结点三个结点,如果叔叔黑色我们就进行对应的旋转操作,spinTree函数我们会在后续详细介绍,如果叔叔结点是红色,我们反转三者的颜色后再检测一次祖父结点
下面是旋转操作的实现,红黑树的旋转逻辑与avl树一样,详细可查看详细介绍平衡二叉树 c++实现-优快云博客
BinaryTreeNode* BinarySearchTree::spinTree(BinaryTreeNode *node){
if (node == nullptr) return nullptr;
int balance = getBalanceFactor(node);
int leftBalance = getBalanceFactor(node->left);
int rightBalance = getBalanceFactor(node->right);
if (balance > 1 && leftBalance >= 0) {
std::cout << "Tree is unbalanced LL: " << node->data << std::endl;
node->isRed = !node->isRed;// 旋转点
node->left->isRed = !node->left->isRed;// 旋转中心点
return spinTreeLL(node);
}
if (balance > 1 && leftBalance < 0) {
std::cout << "Tree is unbalanced LR: " << node->data << std::endl;
node->isRed = !node->isRed;// 旋转点
return spinTreeLR(node);
}
if (balance < -1 && rightBalance <= 0) {
std::cout << "Tree is unbalanced RR: " << node->data << std::endl;
node->isRed = !node->isRed;// 旋转点
node->right->isRed = !node->right->isRed;// 旋转中心点
return spinTreeRR(node);
}
if (balance < -1 && rightBalance > 0) {
std::cout << "Tree is unbalanced RL: " << node->data << std::endl;
node->isRed = !node->isRed;// 旋转点
return spinTreeRL(node);
}
return node;
}
BinaryTreeNode* BinarySearchTree::spinTreeLR(BinaryTreeNode *root){
root->left = spinTreeRR(root->left);
root->left->isRed = !root->left->isRed;// 旋转中心点
return spinTreeLL(root);
}
BinaryTreeNode *BinarySearchTree::spinTreeRL(BinaryTreeNode *root){
root->right = spinTreeLL(root->right);
root->right->isRed = !root->right->isRed;// 旋转中心点
return spinTreeRR(root);
}
删除
红黑树中插入一个结点最容易出现两个连续的红色结点,违背红黑树的性质3(红黑树中不存在两个相邻的红色结点)。而删除操作,最容易造成子树黑高(Black Height)的变化(删除黑色结点可能导致根结点到叶结点黑色结点的数目减少,即黑高降低)。
这是相当麻烦的事情要比插入难很多,红黑树的删除操作是建立在二叉搜索树的基础上的,删除完后检测几种情况进行调整,总体分为下面几种情况
- 删除节点是叶子节点并且是红色:这种情况什么也不需要干,直接删除就可以了
- 删除节点只有一个孩子:这种情况我们只需要将替换上来的节点变黑就可以
- 删除节点是黑色节点:这是最麻烦的情况,因为会删除会违反第五条规则,我们想要根据下面的规则进行调整
- 删除节点的兄弟是黑节点且有红孩子:我们需要检查父节点的平衡因子进行相应的旋转,旋转后再变色
- 删除节点的兄弟是黑节点且孩子都是黑色的:兄弟变红后再检查父节点是否符合规则
- 删除节点的兄弟是红节点:兄弟与父节点变色再进行对应的旋转
![[红黑树删除规则.jpg]]
BinaryTreeNode *BinarySearchTree::_remove(BinaryTreeNode *root, int data) {
if (root == nullptr) return nullptr;
if (data < root->data) {
BinaryTreeNode* temp = _remove(root->left, data);
if (root != _rootNode)root->left = temp;
} else if (data > root->data) {
BinaryTreeNode* temp = _remove(root->right, data);
if (root != _rootNode)root->right = temp;
} else {
// 找到要删除的节点
if (root->left == nullptr || root->right == nullptr) {
BinaryTreeNode *temp = root->left ? root->left : root->right;
if (temp == nullptr) {// 没有孩子的红结点直接删除,黑结点比较麻烦
temp = root;
root = nullptr;
_fixDelete(temp);
std::cout << "f222ather->parent->left: " << _rootNode->left->data << " father->l: " << _rootNode->right->data << std::endl;
} else {// 只有一个孩子的结点,替换后变黑
// *root = *temp;
root->data = temp->data;
root->height = temp->height;
root->left = temp->left;
root->right = temp->right;
root->isRed = false;
}
delete temp;
} else {
BinaryTreeNode *temp = _findMin(root->right);
root->data = temp->data;
root->right = _remove(root->right, temp->data);
}
}
if (root == nullptr) return nullptr;
// 更新节点高度
root->height = 1 + std::max(getHeight(root->left), getHeight(root->right));
return root;
}
void BinarySearchTree::_fixDelete(BinaryTreeNode *node) {
if (node->isRed) return;
BinaryTreeNode* father = node->parent;
BinaryTreeNode* brother = father->left == node ? father->right : father->left;
std::cout << "father: " << father->data << " brother: " << brother->data << " node: " << node->data << std::endl;
//兄弟是红色
if (brother->isRed){
brother->isRed = false;
father->isRed = !father->isRed;
if (father->left == node){
if (father == _rootNode){
_rootNode = spinTreeRR(father);
}else {
father->parent->left == father ?
father->parent->left = spinTreeRR(father) :
father->parent->right = spinTreeRR(father);
}
}else{
if (father == _rootNode){
_rootNode = spinTreeLL(father);
}else {
father->parent->left == father ?
father->parent->left = spinTreeLL(father) :
father->parent->right = spinTreeLL(father);
}
}
_fixDelete(node);
return;
}
//兄弟是黑色并且有两个黑孩子
if ((brother->left == nullptr || !brother->left->isRed) && (brother->right == nullptr || !brother->right->isRed)){
if (!father->isRed){
brother->isRed = true;
_fixDelete(father);
}else{
brother->isRed = true;
father->isRed = false;
}
return;
}
_fixDeleteBrother(node, brother, father);
}
void BinarySearchTree::_fixDeleteBrother(BinaryTreeNode *node, BinaryTreeNode *brother, BinaryTreeNode *father){
if (node == father->left){
father->left = nullptr;
if (brother->right == nullptr || !brother->right->isRed) {
brother->left->isRed = father->isRed;
father->isRed = false;
std::cout << "_fixDelete RL" << std::endl;
brother->left->isRed = father->isRed;
father->isRed = false;
if (father == _rootNode){
_rootNode = spinTreeRLNoColor(father);
}else {
father->parent->left == father ?
father->parent->left = spinTreeRLNoColor(father) :
father->parent->right = spinTreeRLNoColor(father);
}
return;
}
std::cout << "_fixDelete RR" << std::endl;
brother->isRed = father->isRed;
father->isRed = false;
brother->right->isRed = brother->isRed;
if (father == _rootNode){
_rootNode = spinTreeRR(father);
}else {
father->parent->left == father ?
father->parent->left = spinTreeRR(father) :
father->parent->right = spinTreeRR(father);
}
}else{
father->right = nullptr;
if (brother->left == nullptr || !brother->left->isRed) {
std::cout << "_fixDelete LR" << std::endl;
brother->right->isRed = father->isRed;
father->isRed = false;
if (father == _rootNode){
_rootNode = spinTreeLRNoColor(father);
}else {
father->parent->left == father ?
father->parent->left = spinTreeLRNoColor(father) :
father->parent->right = spinTreeLRNoColor(father);
}
return;
}
std::cout << "_fixDelete LL" << std::endl;
brother->isRed = father->isRed;
father->isRed = false;
brother->left->isRed = brother->isRed;
if (father == _rootNode){
_rootNode = spinTreeLL(father);
}else {
father->parent->left == father ?
father->parent->left = spinTreeLL(father) :
father->parent->right = spinTreeLL(father);
}
}
}
这段代码实现了红黑树的删除操作,核心函数是_remove
,它接收一个根节点和要删除的数据作为参数,递归地在树中查找并删除目标节点,同时维持树的平衡。在_remove
函数中,先判断目标数据与当前节点数据的大小关系,若小于当前节点数据则递归删除左子树中的目标节点,若大于则递归删除右子树中的目标节点。当找到目标节点时,根据其孩子节点的情况进行不同处理。若节点只有一个孩子或没有孩子,就用孩子节点替换该节点,对于没有孩子的黑色节点,会调用_fixDelete
函数进行调整。若节点有两个孩子,就用其右子树中的最小节点替换该节点,并递归删除右子树中的最小节点。
_fixDelete
函数用于处理删除黑色节点后的红黑树平衡问题。它接收一个要删除的节点作为参数,先判断节点是否为红色,若是红色则直接返回,因为删除红色节点不会破坏红黑树的性质。若为黑色,找到其兄弟节点,根据兄弟节点的颜色和孩子节点的颜色进行不同处理。若兄弟节点为红色,就将兄弟节点变黑,父节点变红,并进行相应的旋转操作,然后递归调用_fixDelete
函数。若兄弟节点为黑色且有两个黑色孩子,根据父节点的颜色进行不同调整,若父节点为黑色则将兄弟节点变红并递归调整父节点,若父节点为红色则将兄弟节点变红、父节点变黑。若以上情况都不满足,则调用_fixDeleteBrother
函数进一步处理。
_fixDeleteBrother
函数根据要删除节点是父节点的左孩子还是右孩子进行不同的处理。若要删除节点是左孩子,当兄弟节点的右孩子为黑色时进行 RL 旋转,否则进行 RR 旋转;若要删除节点是右孩子,当兄弟节点的左孩子为黑色时进行 LR 旋转,否则进行 LL 旋转。旋转操作中会调整节点的颜色和父节点的指针,以保证红黑树的性质。节点结构BinaryTreeNode
包含了节点的颜色、高度、数据、左右孩子指针和父节点指针,方便在删除操作中进行节点的替换和树的调整。