当前位置: 首页 > news >正文

【数据结构】红黑树(C++实现)

在这里插入图片描述

​📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:数据结构
🎯长路漫漫浩浩,万事皆有期待

上一篇博客:【数据结构】AVL树(C++实现)

文章目录

  • 红黑树的概念
  • 红黑树的性质
  • 红黑树结点的定义
  • 红黑树的插入
  • 红黑树的验证
  • 红黑树的查找
  • 红黑树的删除
  • 红黑树与AVL树的比较
  • 总结:

红黑树的概念

红黑树是一种二叉搜索树,但在每个结点上增加了一个存储位用于表示结点的颜色,这个颜色可以是红色的,也可以是黑色的,因此我们称之为红黑树。

红黑树通过对任何一条从根到叶子的路径上各个结点着色方式的限制,确保没有一条路径会比其他路径长出两倍,因此红黑树是近似平衡的。
在这里插入图片描述

红黑树的性质

红黑树有以下五点性质:

每个结点不是红色就是黑色。
根结点是黑色的。
如果一个结点是红色的,则它的两个孩子结点是黑色的。
对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点。
每个叶子结点都是黑色的(此处的叶子结点指定是空结点)。

红黑树如何确保从根到叶子的最长可能路径不会超过最短可能路径的两倍?

根据红黑树的性质3可以得出,红黑树当中不会出现连续的红色结点,而根据性质4又可以得出,从某一结点到其后代叶子结点的所有路径上包含的黑色结点的数目是相同的。

我们假设在红黑树中,从根到叶子的所有路径上包含的黑色结点的个数都是N个,那么最短路径就是全部由黑色结点构成的路径,即长度为N。
在这里插入图片描述

而最长可能路径就是由一黑一红结点构成的路径,该路径当中黑色结点与红色结点的数目相同,即长度为2N。
在这里插入图片描述

因此,红黑树从根到叶子的最长可能路径不会超过最短可能路径的两倍。

红黑树结点的定义

我们这里直接实现KV模型的红黑树,为了方便后序的旋转操作,将红黑树的结点定义为三叉链结构,除此之外还新加入了一个成员变量,用于表示结点的颜色。

template<class K, class V>
struct RBTreeNode
{//三叉链RBTreeNode<K, V>* _left;RBTreeNode<K, V>* _right;RBTreeNode<K, V>* _parent;//存储的键值对pair<K, V> _kv;//结点的颜色int _col; //红/黑//构造函数RBTreeNode(const pair<K, V>& kv):_left(nullptr), _right(nullptr), _parent(nullptr), _kv(kv), _col(RED){}
};

在这里我们可以使用枚举来定义结点的颜色,这样可以增加代码的可读性和可维护性,并且便于后序的调试操作。

//枚举定义结点的颜色
enum Colour
{RED,BLACK
};

为什么构造结点时,默认将结点的颜色设置为红色?

当我们向红黑树插入结点时,若我们插入的是黑色结点,那么插入路径上黑色结点的数目就比其他路径上黑色结点的数目多了一个,即破坏了红黑树的性质4,此时我们就需要对红黑树进行调整。

若我们插入红黑树的结点是红色的,此时如果其父结点也是红色的,那么表明出现了连续的红色结点,即破坏了红黑树的性质3,此时我们需要对红黑树进行调整;但如果其父结点是黑色的,那我们就无需对红黑树进行调整,插入后仍满足红黑树的要求。

总结一下:

插入黑色结点,一定破坏红黑树的性质4,必须对红黑树进行调整。
插入红色结点,可能破坏红黑树的性质3,可能对红黑树进行调整。

权衡利弊后,我们在构造结点进行插入时,默认将结点的颜色设置为红色。

红黑树的插入

红黑树插入结点的逻辑分为三步:

按二叉搜索树的插入方法,找到待插入位置。
将待插入结点插入到树中。
若插入结点的父结点是红色的,则需要对红黑树进行调整。

其中前两步与二叉搜索树插入结点时的逻辑相同,红黑树的关键在于第三步对红黑树的调整。

红黑树在插入结点后是如何调整的?

实际上,在插入结点后并不是一定会对红黑树进行调整,若插入结点的父结点是黑色的,那么我们就不用对红黑树进行调整,因为本次结点的插入并没有破坏红黑树的五点性质。

只有当插入结点的父结点是红色时才需要对红黑树进行调整,因为我们默认插入的结点就是红色的,如果插入结点的父结点也是红色的,那么此时就出现了连续的红色结点,因此需要对红黑树进行调整。

因为插入结点的父结点是红色的,说明父结点不是根结点(根结点是黑色的),因此插入结点的祖父结点(父结点的父结点)就一定存在。

红黑树调整时具体应该如何调整,主要是看插入结点的叔叔(插入结点的父结点的兄弟结点),根据插入结点叔叔的不同,可将红黑树的调整分为三种情况。

情况一:插入结点的叔叔存在,且叔叔的颜色是红色。

此时为了避免出现连续的红色结点,我们可以将父结点变黑,但为了保持每条路径黑色结点的数目不变,因此我们还需要将祖父结点变红,再将叔叔变黑。这样一来既保持了每条路径黑色结点的数目不变,也解决了连续红色结点的问题。
在这里插入图片描述
但调整还没有结束,因为此时祖父结点变成了红色,如果祖父结点是根结点,那我们直接再将祖父结点变成黑色即可,此时相当于每条路径黑色结点的数目都增加了一个。

但如果祖父结点不是根结点的话,我们就需要将祖父结点当作新插入的结点,再判断其父结点是否为红色,若其父结点也是红色,那么又需要根据其叔叔的不同,进而进行不同的调整操作。

因此,情况一的抽象图表示如下:
在这里插入图片描述

注意: 叔叔存在且为红时,cur结点是parent的左孩子还是右孩子,调整方法都是一样的。

情况二:插入结点的叔叔存在,且叔叔的颜色是黑色。

这种情况一定是在情况一继续往上调整的过程中出现的,即这种情况下的cur结点一定不是新插入的结点,而是上一次情况一调整过程中的祖父结点,如下图:

在这里插入图片描述

我们将路径中祖父结点之上黑色结点的数目设为x xx,将叔叔结点之下黑色结点的数目设为y yy,则在插入结点前,图示两条路径黑色结点的数目分别为x+1 和x+2+y,很明显x+2+y 是一定大于 x+1的,因此在插入结点前就不满足红黑树的要求了,所以说叔叔结点存在且为黑这种情况,一定是由情况一往上调整过程中才会出现的一种情况。

需要注意:

从根结点一直走到空位置就算一条路径,而不是从根结点走到左右结点均为空的叶子结点时才算一条路径。
情况二和情况三均需要进行旋转处理,旋转处理后无需继续往上进行调整,所以说情况二一定是由情况一往上调整的过程中出现的。

出现叔叔存在且为黑时,单纯使用变色已经无法处理了,这时我们需要进行旋转处理。若祖孙三代的关系是直线(cur、parent、grandfather这三个结点为一条直线),则我们需要先进行单旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根结点是黑色的,因此无需继续往上进行处理。

抽象图表示如下:
在这里插入图片描述
说明一下: 当直线关系为,parent是grandfather的右孩子,cur是parent的右孩子时,就需要先进行左单旋操作,再进行颜色调整。

若祖孙三代的关系是折现(cur、parent、grandfather这三个结点为一条折现),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理。

抽象图表示如下:
在这里插入图片描述

说明一下: 当折现关系为,parent是grandfather的右孩子,cur是parent的左孩子时,就需要先进行右左双旋操作,再进行颜色调整。

情况三:插入结点的叔叔不存在。

在这种情况下的cur结点一定是新插入的结点,而不可能是由情况一变化而来的,因为叔叔不存在说明在parent的下面不可能再挂黑色结点了,如下图:
在这里插入图片描述

如果插入前parent下面再挂黑色结点,就会导致图中两条路径黑色结点的数目不相同,而parent是红色的,因此parent下面自然也不能挂红色结点,所以说这种情况下的cur结点一定是新插入的结点。

和情况二一样,若祖孙三代的关系是直线(cur、parent、grandfather这三个结点为一条直线),则我们需要先进行单旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根结点是黑色的,因此无需继续往上进行处理。

抽象图表示如下:
在这里插入图片描述

说明一下: 当直线关系为,parent是grandfather的右孩子,cur是parent的右孩子时,就需要先进行左单旋操作,再进行颜色调整。

若祖孙三代的关系是折现(cur、parent、grandfather这三个结点为一条折现),则我们需要先进行双旋操作,再进行颜色调整,颜色调整后这棵被旋转子树的根是黑色的,因此无需继续往上进行处理。

抽象图表示如下:
在这里插入图片描述

说明一下: 当折现关系为,parent是grandfather的右孩子,cur是parent的左孩子时,就需要先进行右左双旋操作,再进行颜色调整。

代码如下:

//插入函数
pair<Node*, bool> Insert(const pair<K, V>& kv)
{if (_root == nullptr) //若红黑树为空树,则插入结点直接作为根结点{_root = new Node(kv);_root->_col = BLACK; //根结点必须是黑色return make_pair(_root, true); //插入成功}//1、按二叉搜索树的插入方法,找到待插入位置Node* cur = _root;Node* parent = nullptr;while (cur){if (kv.first < cur->_kv.first) //待插入结点的key值小于当前结点的key值{//往该结点的左子树走parent = cur;cur = cur->_left;}else if (kv.first > cur->_kv.first) //待插入结点的key值大于当前结点的key值{//往该结点的右子树走parent = cur;cur = cur->_right;}else //待插入结点的key值等于当前结点的key值{return make_pair(cur, false); //插入失败}}//2、将待插入结点插入到树中cur = new Node(kv); //根据所给值构造一个结点Node* newnode = cur; //记录新插入的结点(便于后序返回)if (kv.first < parent->_kv.first) //新结点的key值小于parent的key值{//插入到parent的左边parent->_left = cur;cur->_parent = parent;}else //新结点的key值大于parent的key值{//插入到parent的右边parent->_right = cur;cur->_parent = parent;}//3、若插入结点的父结点是红色的,则需要对红黑树进行调整while (parent&&parent->_col == RED){Node* grandfather = parent->_parent; //parent是红色,则其父结点一定存在if (parent == grandfather->_left) //parent是grandfather的左孩子{Node* uncle = grandfather->_right; //uncle是grandfather的右孩子if (uncle&&uncle->_col == RED) //情况1:uncle存在且为红{//颜色调整parent->_col = uncle->_col = BLACK;grandfather->_col = RED;//继续往上处理cur = grandfather;parent = cur->_parent;}else //情况2+情况3:uncle不存在 + uncle存在且为黑{if (cur == parent->_left){RotateR(grandfather); //右单旋//颜色调整grandfather->_col = RED;parent->_col = BLACK;}else //cur == parent->_right{RotateLR(grandfather); //左右双旋//颜色调整grandfather->_col = RED;cur->_col = BLACK;}break; //子树旋转后,该子树的根变成了黑色,无需继续往上进行处理}}else //parent是grandfather的右孩子{Node* uncle = grandfather->_left; //uncle是grandfather的左孩子if (uncle&&uncle->_col == RED) //情况1:uncle存在且为红{//颜色调整uncle->_col = parent->_col = BLACK;grandfather->_col = RED;//继续往上处理cur = grandfather;parent = cur->_parent;}else //情况2+情况3:uncle不存在 + uncle存在且为黑{if (cur == parent->_left){RotateRL(grandfather); //右左双旋//颜色调整cur->_col = BLACK;grandfather->_col = RED;}else //cur == parent->_right{RotateL(grandfather); //左单旋//颜色调整grandfather->_col = RED;parent->_col = BLACK;}break; //子树旋转后,该子树的根变成了黑色,无需继续往上进行处理}}}_root->_col = BLACK; //根结点的颜色为黑色(可能被情况一变成了红色,需要变回黑色)return make_pair(newnode, true); //插入成功
}//左单旋
void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;Node* parentParent = parent->_parent;//建立subRL与parent之间的联系parent->_right = subRL;if (subRL)subRL->_parent = parent;//建立parent与subR之间的联系subR->_left = parent;parent->_parent = subR;//建立subR与parentParent之间的联系if (parentParent == nullptr){_root = subR;_root->_parent = nullptr;}else{if (parent == parentParent->_left){parentParent->_left = subR;}else{parentParent->_right = subR;}subR->_parent = parentParent;}
}//右单旋
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;Node* parentParent = parent->_parent;//建立subLR与parent之间的联系parent->_left = subLR;if (subLR)subLR->_parent = parent;//建立parent与subL之间的联系subL->_right = parent;parent->_parent = subL;//建立subL与parentParent之间的联系if (parentParent == nullptr){_root = subL;_root->_parent = nullptr;}else{if (parent == parentParent->_left){parentParent->_left = subL;}else{parentParent->_right = subL;}subL->_parent = parentParent;}
}//左右双旋
void RotateLR(Node* parent)
{RotateL(parent->_left);RotateR(parent);
}//右左双旋
void RotateRL(Node* parent)
{RotateR(parent->_right);RotateL(parent);
}

注意: 在红黑树调整后,需要将根结点的颜色变为黑色,因为红黑树的根结点可能在情况一的调整过程中被变成了红色。

红黑树的验证

红黑树也是一种特殊的二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断该二叉树是否满足二叉搜索树的性质。

代码如下:

//中序遍历
void Inorder()
{_Inorder(_root);
}
//中序遍历子函数
void _Inorder(Node* root)
{if (root == nullptr)return;_Inorder(root->_left);cout << root->_kv.first << " ";_Inorder(root->_right);
}

但中序有序只能证明是二叉搜索树,要证明二叉树是红黑树还需验证该二叉树是否满足红黑树的性质。

代码如下:

//判断是否为红黑树
bool ISRBTree()
{if (_root == nullptr) //空树是红黑树{return true;}if (_root->_col == RED){cout << "error:根结点为红色" << endl;return false;}//找最左路径作为黑色结点数目的参考值Node* cur = _root;int BlackCount = 0;while (cur){if (cur->_col == BLACK)BlackCount++;cur = cur->_left;}int count = 0;return _ISRBTree(_root, count, BlackCount);
}
//判断是否为红黑树的子函数
bool _ISRBTree(Node* root, int count, int BlackCount)
{if (root == nullptr) //该路径已经走完了{if (count != BlackCount){cout << "error:黑色结点的数目不相等" << endl;return false;}return true;}if (root->_col == RED&&root->_parent->_col == RED){cout << "error:存在连续的红色结点" << endl;return false;}if (root->_col == BLACK){count++;}return _ISRBTree(root->_left, count, BlackCount) && _ISRBTree(root->_right, count, BlackCount);
}

红黑树的查找

红黑树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:

若树为空树,则查找失败,返回nullptr。
若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
若key值等于当前结点的值,则查找成功,返回对应结点。

代码如下:

//查找函数
Node* Find(const K& key)
{Node* cur = _root;while (cur){if (key < cur->_kv.first) //key值小于该结点的值{cur = cur->_left; //在该结点的左子树当中查找}else if (key > cur->_kv.first) //key值大于该结点的值{cur = cur->_right; //在该结点的右子树当中查找}else //找到了目标结点{return cur; //返回该结点}}return nullptr; //查找失败
}

红黑树的删除

红黑树的删除要比插入更加难以理解,但是只要仔细一点也还行。

第一步:找到实际待删除的结点

找结点的过程与二叉搜索树寻找待删除结点的方法一样,若找到的待删除结点的左右子树均不为空,则需要使用替换法进行删除。因此我们最终需要删除的都是左右子树至少有一个为空的结点。

找到实际待删除结点后,先不删除该结点,否则调整红黑树时不容易控制,找到实际待删除结点后立即进行红黑树的调整。

第二步:调整红黑树

调整红黑树之前,我们先判断一下本次结点的删除是否会破坏了红黑树的性质,若破坏了我们才需要对红黑树进行调整。

若实际删除的结点是红色结点,那么本次删除操作不会破坏红黑树的性质,因此我们不需要对红黑树进行调整。反之,若删除的结点是黑色结点,我们就需要对红黑树进行调整,因为黑色结点的删除将会使得一些路径中黑色结点的数目减少,此时便破坏了红黑树的性质四。

我们先来说最简单的一种情况,即待删除结点只有一个孩子为空的情况。

在这种情况下,待删除结点要么是只有左孩子,要么是有只右孩子,但不管是左孩子还是右孩子,这个孩子一定是红色的,因为若这个孩子是黑色的,那么此时图示长蓝色路径的黑色结点数目比短蓝色路径的黑色结点数目多,不符合红黑树的性质。
在这里插入图片描述

又因为红黑树当中不允许出现连续的红色结点,因此在这种情况下实际上就只有图示两种实际情况,这时我们直接将待删除结点的那个红孩子变成黑色就行了,因为在后面实际删除结点时会将这个孩子连接到删除结点的父结点下面,连接后相当于我们删除的是一个红色结点,红黑树调整完成。

下面再来说比较复杂的情况,即待删除结点的左右孩子均为空。

我们以待删除结点是其父结点的左孩子为例,分为以下四种情况:

图示说明:

若parent结点为白色,表明parent结点可能是红色结点也可能是黑色结点。
若bL或bR结点为白色,表明其可能是红色结点或黑色结点甚至该结点不存在。
bL和bR结点为黑色时,表明他们可能是黑色结点或该结点不存在。

情况一:brother为红色。
在这里插入图片描述

当待删除结点的brother为红色时,我们先以parent为旋转点进行一次左单旋,再将brother的颜色变为黑色,将parent的颜色变为红色,此时我们再对待删除结点cur进行情况分析,情况一就转换成了情况二、三或四。

情况二:brother为黑色,且其左右孩子都是黑色结点或为空。
在这里插入图片描述

在该情况下,我们直接将brother的颜色变成红色,此时根据parent的颜色决定红黑树的调整是否结束,若parent的颜色是红色,则我们将parent变为黑色后即可结束红黑树的调整;若parent的颜色原本就是黑色,则我们需要将parent结点当作下一次调整时的cur结点进行情况分析,并且情况二在下一次调整时可能会是情况一、二、三、四当中的任何一种。

情况三:brother为黑色,且其左孩子是红色结点,右孩子是黑色结点或为空。
在这里插入图片描述

出现该情况时,我们先以brother为旋转点进行一次右单旋,再将brother结点变为红色,将brotherLeft变为黑色,此时我们再对待删除结点cur进行情况分析,情况三就转换成了情况四。

情况四:brother为黑色,且其右孩子是红色结点。
在这里插入图片描述

经过情况四的处理后,红黑树就一定调整结束了。在情况四当中,我们先以parent为旋转点进行一次左单旋,然后将parent的颜色赋值给brother,再将parent的颜色变为黑色,最后将brotherRight变为黑色,此时红黑树的调整便结束了。

说明一下:

待删除结点是其父结点的右孩子时的四种情况与上面四种情况类似,这里就不列举出来了。
若待删除结点没有父结点,即待删除结点是根结点时,在找到该结点时就进行了删除,这里不用考虑,具体看代码。

这里有必要对各种情况的切换进行说明,你可能会担心调整红黑树时在这四种情况当中一直来回切换而不能跳出,下面我们来对此进行分析:
在这里插入图片描述

首先,进入情况四后红黑树就一定调整结束了。其次,进入情况三后,下次也一定会进入情况四,红黑树的调整也会结束。所以情况三和情况四是没有问题的,你们最纠结的只能是情况一和情况二了。

情况一又会切换为情况二、三、四,因此只要情况二能够有办法退出,那么所有情况就都能退出了。

在情况二当中我们说,如果parent的颜色是红色,那么我们将parent变为黑色后就可以结束红黑树的调整,那会不会每次进入情况二时parent的颜色都不是红色,而一直是黑色的呢?

当然有可能,但是我们若一直往上进行调整时,那么总会调整到红黑树的根结点,当调整到根结点后我们便不用进行调整了,此时根结点虽然是黑色的,但是不影响,这仅仅意味着每条从根到叶子的路径上包含的黑色结点的个数都减少了一个,此时也没有破坏红黑树的性质,也就完成了红黑树的调整,因此在调整过程中不会出现一直在这四种情况来回切换而不能跳出的问题。

第三步:进行结点的实际删除

在红黑树调整完毕后,我们就可以进行结点的删除了,删除结点的方式很简单,若待删除结点有左孩子或右孩子,我们将其左孩子或右孩子连接到待删除结点父结点的下面即可,之后便可以将待删除结点删除了。

代码如下:

//删除函数
bool Erase(const K& key)
{//用于遍历二叉树Node* parent = nullptr;Node* cur = _root;//用于标记实际的待删除结点及其父结点Node* delParentPos = nullptr;Node* delPos = nullptr;while (cur){if (key < cur->_kv.first) //所给key值小于当前结点的key值{//往该结点的左子树走parent = cur;cur = cur->_left;}else if (key > cur->_kv.first) //所给key值大于当前结点的key值{//往该结点的右子树走parent = cur;cur = cur->_right;}else //找到了待删除结点{if (cur->_left == nullptr) //待删除结点的左子树为空{if (cur == _root) //待删除结点是根结点{_root = _root->_right; //让根结点的右子树作为新的根结点if (_root){_root->_parent = nullptr;_root->_col = BLACK; //根结点为黑色}delete cur; //删除原根结点return true;}else{delParentPos = parent; //标记实际删除结点的父结点delPos = cur; //标记实际删除的结点}break; //进行红黑树的调整以及结点的实际删除}else if (cur->_right == nullptr) //待删除结点的右子树为空{if (cur == _root) //待删除结点是根结点{_root = _root->_left; //让根结点的左子树作为新的根结点if (_root){_root->_parent = nullptr;_root->_col = BLACK; //根结点为黑色}delete cur; //删除原根结点return true;}else{delParentPos = parent; //标记实际删除结点的父结点delPos = cur; //标记实际删除的结点}break; //进行红黑树的调整以及结点的实际删除}else //待删除结点的左右子树均不为空{//替换法删除//寻找待删除结点右子树当中key值最小的结点作为实际删除结点Node* minParent = cur;Node* minRight = cur->_right;while (minRight->_left){minParent = minRight;minRight = minRight->_left;}cur->_kv.first = minRight->_kv.first; //将待删除结点的key改为minRight的keycur->_kv.second = minRight->_kv.second; //将待删除结点的value改为minRight的valuedelParentPos = minParent; //标记实际删除结点的父结点delPos = minRight; //标记实际删除的结点break; //进行红黑树的调整以及结点的实际删除}}}if (delPos == nullptr) //delPos没有被修改过,说明没有找到待删除结点{return false;}//记录待删除结点及其父结点(用于后续实际删除)Node* del = delPos;Node* delP = delParentPos;//调整红黑树if (delPos->_col == BLACK) //删除的是黑色结点{if (delPos->_left) //待删除结点有一个红色的左孩子(不可能是黑色){delPos->_left->_col = BLACK; //将这个红色的左孩子变黑即可}else if (delPos->_right) //待删除结点有一个红色的右孩子(不可能是黑色){delPos->_right->_col = BLACK; //将这个红色的右孩子变黑即可}else //待删除结点的左右均为空{while (delPos != _root) //可能一直调整到根结点{if (delPos == delParentPos->_left) //待删除结点是其父结点的左孩子{Node* brother = delParentPos->_right; //兄弟结点是其父结点的右孩子//情况一:brother为红色if (brother->_col == RED){delParentPos->_col = RED;brother->_col = BLACK;RotateL(delParentPos);//需要继续处理brother = delParentPos->_right; //更新brother(否则在本循环中执行其他情况的代码会出错)}//情况二:brother为黑色,且其左右孩子都是黑色结点或为空if (((brother->_left == nullptr) || (brother->_left->_col == BLACK))&& ((brother->_right == nullptr) || (brother->_right->_col == BLACK))){brother->_col = RED;if (delParentPos->_col == RED){delParentPos->_col = BLACK;break;}//需要继续处理delPos = delParentPos;delParentPos = delPos->_parent;}else{//情况三:brother为黑色,且其左孩子是红色结点,右孩子是黑色结点或为空if ((brother->_right == nullptr) || (brother->_right->_col == BLACK)){brother->_left->_col = BLACK;brother->_col = RED;RotateR(brother);//需要继续处理brother = delParentPos->_right; //更新brother(否则执行下面情况四的代码会出错)}//情况四:brother为黑色,且其右孩子是红色结点brother->_col = delParentPos->_col;delParentPos->_col = BLACK;brother->_right->_col = BLACK;RotateL(delParentPos);break; //情况四执行完毕后调整一定结束}}else //delPos == delParentPos->_right //待删除结点是其父结点的左孩子{Node* brother = delParentPos->_left; //兄弟结点是其父结点的左孩子//情况一:brother为红色if (brother->_col == RED) //brother为红色{delParentPos->_col = RED;brother->_col = BLACK;RotateR(delParentPos);//需要继续处理brother = delParentPos->_left; //更新brother(否则在本循环中执行其他情况的代码会出错)}//情况二:brother为黑色,且其左右孩子都是黑色结点或为空if (((brother->_left == nullptr) || (brother->_left->_col == BLACK))&& ((brother->_right == nullptr) || (brother->_right->_col == BLACK))){brother->_col = RED;if (delParentPos->_col == RED){delParentPos->_col = BLACK;break;}//需要继续处理delPos = delParentPos;delParentPos = delPos->_parent;}else{//情况三:brother为黑色,且其右孩子是红色结点,左孩子是黑色结点或为空if ((brother->_left == nullptr) || (brother->_left->_col == BLACK)){brother->_right->_col = BLACK;brother->_col = RED;RotateL(brother);//需要继续处理brother = delParentPos->_left; //更新brother(否则执行下面情况四的代码会出错)}//情况四:brother为黑色,且其左孩子是红色结点brother->_col = delParentPos->_col;delParentPos->_col = BLACK;brother->_left->_col = BLACK;RotateR(delParentPos);break; //情况四执行完毕后调整一定结束}}}}}//进行实际删除if (del->_left == nullptr) //实际删除结点的左子树为空{if (del == delP->_left) //实际删除结点是其父结点的左孩子{delP->_left = del->_right;if (del->_right)del->_right->_parent = delP;}else //实际删除结点是其父结点的右孩子{delP->_right = del->_right;if (del->_right)del->_right->_parent = delP;}}else //实际删除结点的右子树为空{if (del == delP->_left) //实际删除结点是其父结点的左孩子{delP->_left = del->_left;if (del->_left)del->_left->_parent = delP;}else //实际删除结点是其父结点的右孩子{delP->_right = del->_left;if (del->_left)del->_left->_parent = delP;}}delete del; //实际删除结点return true;
}

红黑树与AVL树的比较

红黑树和AVL树都是高效的平衡二叉树,增删查改的时间复杂度都是O(logN),但红黑树和AVL树控制二叉树平衡的方式不同:

AVL树是通过控制左右高度差不超过1来实现二叉树平衡的,实现的是二叉树的严格平衡。
红黑树是通过控制结点的颜色,从而使得红黑树当中最长可能路径不超过最短可能路径的2倍,实现的是近似平衡。
相对于AVL树来说,红黑树降低了插入结点时需要进行的旋转的次数,所以在经常进行增删的结构中性能比AVL树更优,实际运用时也大多用的是红黑树。

总结:

今天我们比较详细地完成了红黑树的C++实现,了解了一些有关的底层原理。接下来,我们将进行STL中 set、map、multiset、multimap类的学习。希望我的文章和讲解能对大家的学习提供一些帮助。

当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~

在这里插入图片描述

相关文章:

【数据结构】红黑树(C++实现)

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;数据结构 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【数据…...

day-64 代码随想录算法训练营(19)图论 part 03

827.最大人工岛 思路一&#xff1a;深度优先遍历 1.深度优先遍历&#xff0c;求出所有岛屿的面积&#xff0c;并且把每个岛屿记上不同标记2.使用 unordered_map 使用键值对&#xff0c;标记&#xff1a;面积&#xff0c;记录岛屿面积3.遍历所有海面&#xff0c;然后进行一次广…...

xss测试步骤总结

文章目录 测试流程1.开启burp2.测试常规xss语句3.观察回显4.测试闭合与绕过Level2Level3Level4Level5Level6Level7 5.xss绕过方法1)测试需观察点2)无过滤法3)">闭合4)单引号闭合事件函数5)双引号闭合事件函数6)引号闭合链接7)大小写绕过8)多写绕过9)unicode编码10)unic…...

2023最新简易ChatGPT3.5小程序全开源源码+全新UI首发+实测可用可二开(带部署教程)

源码简介&#xff1a; 2023最新简易ChatGPT3.5小程序全开源源码全新UI首发&#xff0c;实测可以用&#xff0c;而且可以二次开发。这个是最新ChatGPT智能AI机器人微信小程序源码&#xff0c;同时也带部署教程。 这个全新版本的小界面设计相当漂亮&#xff0c;简单大方&#x…...

【Redis】数据过期策略和数据淘汰策略

数据过期策略和淘汰策略 过期策略 Redis所有的数据结构都可以设置过期时间&#xff0c;时间一到&#xff0c;就会自动删除。 问题&#xff1a;大家都知道&#xff0c;Redis是单线程的&#xff0c;如果同一时间太多key过期&#xff0c;Redis删除的时间也会占用线程的处理时间…...

RPA的优势和劣势是什么,RPA能力边界在哪里?

RPA&#xff0c;即Robotic Process Automation&#xff08;机器人流程自动化&#xff09;&#xff0c;是一种新型的自动化技术&#xff0c;它可以通过软件机器人模拟人类在计算机上执行的操作&#xff0c;从而实现业务流程的自动化。RPA技术的出现&#xff0c;为企业提高效率、…...

Kubernetes 学习总结(38)—— Kubernetes 与云原生的联系

一、什么是云原生&#xff1f; 伴随着云计算的浪潮&#xff0c;云原生概念也应运而生&#xff0c;而且火得一塌糊涂&#xff0c;大家经常说云原生&#xff0c;却很少有人告诉你到底什么是云原生&#xff0c;云原生可以理解为“云”“原生”&#xff0c;Cloud 可以理解为应用程…...

号卡推广管理系统源码/手机流量卡推广网站源码/PHP源码+带后台版本+分销系统

源码简介&#xff1a; 号卡推广管理系统源码/手机流量卡推广网站源码&#xff0c;基于PHP源码&#xff0c;而且它是带后台版本&#xff0c;分销系统。运用全新UI流量卡官网系统源码有后台带文章。 这个流量卡销售网站源码&#xff0c;PHP流量卡分销系统&#xff0c;它可以支持…...

【C语言】汉诺塔 —— 详解

一、介绍 汉诺塔&#xff08;Tower of Hanoi&#xff09;&#xff0c;又称河内塔&#xff0c;是一个源于印度古老传说的益智玩具。大焚天创造世界的时候做了三根金刚石柱子&#xff0c;在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。 大焚天命令婆罗门把圆盘从下面开始按…...

【云备份】

文章目录 [toc] 1 :peach:云备份的认识:peach:1.1 :apple:功能了解:apple:1.2 :apple:实现目标:apple:1.3 :apple:服务端程序负责功能:apple:1.4 :apple:服务端功能模块划分:apple:1.5 :apple:客户端程序负责功能:apple:1.6 :apple:客户端功能模块划分:apple: 2 :peach:环境搭建…...

第四十六章 命名空间和数据库 - 系统提供的数据库

文章目录 第四十六章 命名空间和数据库 - 系统提供的数据库系统提供的数据库ENSLIBIRISAUDITIRISLIBIRISLOCALDATAIRISSYS (the system manager’s database 系统管理器的数据库)IRISTEMP 第四十六章 命名空间和数据库 - 系统提供的数据库 系统提供的数据库 IRIS 提供以下数据…...

【贪心的商人】python实现-附ChatGPT解析

1.题目 贪心的商人 知识点:贪心 时间限制:1s 空间限制: 256MB 限定语言:不限 题目描述: 商人经营一家店铺,有number种商品,由于仓库限制 每件商品的最大持有数量是item[index], 每种商品的价格在每天是item_price[item_index][day], 通过对商品的买进和卖出获取利润,请给…...

解决nvm切换node版本失败的终极办法-秒杀网上99%的水文

nvm是一款强大的node多版本管理器&#xff0c;可以轻易选择你需要的node版本&#xff0c;这对win7平台简直就是超好的福音&#xff1a;可以突破node 14.15以上的安装限制。 但是nvm安装有一个巨大的坑点&#xff1a;nvm use 版本号以后&#xff0c;并没有生效&#xff0c;nvm …...

2023蓝帽杯半决赛电子取证+CTF部分题解

文章目录 电子取证123456789101112131415 CTFWeb | MyLinuxBotWeb | AirticleShareCrypto | ezrsaPwn | AdminPwn | uafmisc|排排坐吃吃果果 电子取证 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 CTF Web | MyLinuxBot Web | AirticleShare import requests import times reques…...

OCTA数据集(Rose)+ OCTA-Net

ROSE: A Retinal OCT-Angiography(视网膜眼底相干光层析血管成像术) Vessel Segmentation(血管分割) Dataset and New Model 论文&#xff1a;ROSE: A Retinal OCT-Angiography Vessel Segmentation Dataset and New Model 代码和数据集&#xff1a;ROSE1&2 - 医疗影像/眼…...

java Spring Boot 手动启动热部署

好 接下来 我们讲一个对开发非常重要的东西 热部署 因为 我们在开发过程中总会希望快点看到效果 或者 你的企业项目一般很大很复杂&#xff0c;重启是一件非常麻烦的事 或者你在和前端同事联调&#xff0c;有一点小问题 你改完就要重启 前端还得等你&#xff0c;非常不友好 那…...

Autosar诊断实战系列20-UDS首帧数据接收及流控帧发送代码级分析

本文框架 前言1. 长帧数据的首帧接收2. 首帧数据的处理及流控帧发送2.1 首帧数据的处理2.2 流控帧数据的发送前言 在本系列笔者将结合工作中对诊断实战部分的应用经验进一步介绍常用UDS服务的进一步探讨及开发中注意事项, Dem/Dcm/CanTp/Fim模块配置开发及注意事项,诊断与Bs…...

C/C++ 数据结构 - 队列

1.队列 https://blog.csdn.net/LiuBo_01/article/details/80412290 1 #include <stdio.h>2 #include <stdlib.h>3 4 typedef struct Node5 {6 int data;7 struct Node* next;8 }N;9 10 typedef struct11 {12 N* front;13 N* rear;14 }Q;15 16 //…...

免杀对抗-DLL劫持免杀

C&Py-DLL劫持-语言-调用加载 1.使用visual studio创建项目 2.将文件名重命名为.c后缀 3.将如下加载器代码生成dll文件 加载器代码&#xff1a; #include "pch.h" #include <Windows.h> #include <stdio.h> #include <string.h>#pragma comment…...

Anaconda添加channels后出现unexpected urllib3 DEBUG logging from conda-build

1.问题描述 anaconda更新之后添加channels后出现bug: (base) ~/zlib-feedstock % conda build recipe 2>&1 | tee out ... INFO:conda_build.metadata:Attempting to finalize metadata for libzlib DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1):…...

基于Uniapp开发HarmonyOS 5.0旅游应用技术实践

一、技术选型背景 1.跨平台优势 Uniapp采用Vue.js框架&#xff0c;支持"一次开发&#xff0c;多端部署"&#xff0c;可同步生成HarmonyOS、iOS、Android等多平台应用。 2.鸿蒙特性融合 HarmonyOS 5.0的分布式能力与原子化服务&#xff0c;为旅游应用带来&#xf…...

1.3 VSCode安装与环境配置

进入网址Visual Studio Code - Code Editing. Redefined下载.deb文件&#xff0c;然后打开终端&#xff0c;进入下载文件夹&#xff0c;键入命令 sudo dpkg -i code_1.100.3-1748872405_amd64.deb 在终端键入命令code即启动vscode 需要安装插件列表 1.Chinese简化 2.ros …...

(转)什么是DockerCompose?它有什么作用?

一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用&#xff0c;而无需手动一个个创建和运行容器。 Compose文件是一个文本文件&#xff0c;通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台

🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...

rnn判断string中第一次出现a的下标

# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...

Linux 中如何提取压缩文件 ?

Linux 是一种流行的开源操作系统&#xff0c;它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间&#xff0c;使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的&#xff0c;要在 …...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...

【Veristand】Veristand环境安装教程-Linux RT / Windows

首先声明&#xff0c;此教程是针对Simulink编译模型并导入Veristand中编写的&#xff0c;同时需要注意的是老用户编译可能用的是Veristand Model Framework&#xff0c;那个是历史版本&#xff0c;且NI不会再维护&#xff0c;新版本编译支持为VeriStand Model Generation Suppo…...

绕过 Xcode?使用 Appuploader和主流工具实现 iOS 上架自动化

iOS 应用的发布流程一直是开发链路中最“苹果味”的环节&#xff1a;强依赖 Xcode、必须使用 macOS、各种证书和描述文件配置……对很多跨平台开发者来说&#xff0c;这一套流程并不友好。 特别是当你的项目主要在 Windows 或 Linux 下开发&#xff08;例如 Flutter、React Na…...

DAY 26 函数专题1

函数定义与参数知识点回顾&#xff1a;1. 函数的定义2. 变量作用域&#xff1a;局部变量和全局变量3. 函数的参数类型&#xff1a;位置参数、默认参数、不定参数4. 传递参数的手段&#xff1a;关键词参数5 题目1&#xff1a;计算圆的面积 任务&#xff1a; 编写一…...