目录

红黑树简介
首先我们来聊一聊红黑树是什么
在之前的学习中,我们了解到了二叉搜索树,AVL树
二叉搜索树的特性是左子树 < 根 < 右子树,但是二叉搜索树也有缺点,就是如果处在极端情况下,那么二叉搜索树的效率就和链表差不多
为了解决这个问题,我们学习了AVL树,这棵树是通过旋转来控制树的高度,以此来达到绝对平衡的目的,也就是任何一棵子树,左右高度差都不会超过1
但是这也伴随着一个问题就是,如果我们每一次插入,都判断、旋转,这样子的效率固然是高,但是有没有更好的办法,让其在保持高度平衡的同时,尽量减少旋转的次数
这时候我们就引出了我们的红黑树
AVL树是通过平衡因子来控制树的高度的,而红黑树是一种通过颜色控制高度的树
红黑树有以下四条规则:
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 每条路径均包含相同数目的黑色结点
红色节点是不能与黑色节点相连的,并且,每条路径都有相同黑色节点,这点非常重要
我们来思考一下,为什么通过颜色,就能控制住树的高度,还有就是这样子是否真的能保证树的平衡

注意看,上图是最极限的情况(高度),因为两边都不能再加黑色节点了,每条路径都只有两个黑色节点,如果再加节点的话,在(根节点的)右子树加那么高度就没有差那么多了,不是最极限的情况,如果在(根节点的)左子树插入节点的话,就只能插入黑色的节点,但是单在左树上插入黑色节点,那么其他路径的黑色节点和这个的就不一样了,所以就需要旋转,这样就控制住了高度
而且,红黑树要求的不是绝对的平衡,而是一种相对平衡
他不像AVL树,一有不平衡就旋转,他会到真的不得已了才旋转,旋转次数下来了,效率就高了,并且红黑树还能保持树的高度平衡
所以,在库里面的set、map封装都是拿红黑树做的底层
红黑树框架构建
首先,我们的红黑树是通过颜色控制的,所以我们可以写一个联合体enum,我们后面就可以直接写对应颜色的名字,这样代码的可读性也会提高
enum Colour
{
RED,
BLACK
};
然后我们还需要写一个红黑树的节点类,因为一个节点里面要存指针,存联合体,存pair类型的数据,所以我们就只能写一个类,然后将这个类做为红黑树的成员变量
template<class k, class v>
struct RBTreeNode
{
RBTreeNode<k, v>* _left;// 左指针
RBTreeNode<k, v>* _right;// 右指针
RBTreeNode<k, v>* _parent;// 父节点
Colour _col;// 颜色
pair<k, v> _data;// 数据
RBTreeNode(const pair<k, v>& data)
:_left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _data(data)
{}
// 这里的颜色不需要初始化,我们后面插入节点的时候才初始化
};
最后,我们还需要将这个节点类做为红黑树的成员变量,当然我们在这之前还可以对其typedef一下,可以将其直接typedef成Node,这样我们也好写
然后红黑树这个类需要的是一个节点指针,这个指针指向根节点
template<class K, class V>
class RBTree
{
typedef RBTreeNode<K, V> Node;
public:
private:
Node* _root;
};
构造函数 析构函数
由于我们的成员变量是一个类,默认生成的构造函数会调用这个类的构造,所以我们并不需要显示写构造,所以写不写其实一样
但是考虑到有些人会喜欢加上拷贝构造,那我们就需要强制编译器生成一个构造函数
// 强制编译器生成构造
RBTree() = default;
至于析构的话,我们直接写一个递归即可
先递归左子树,然后是右子树,最后删除当前节点即可,至于递归出口就是,当该节点为空,就退出
当然我们可以写一个函数来完成这个递归的逻辑,最后在函数结束后,我们再将这个根节点置空
~RBTree()
{
Destroy(_root);
_root = nullptr;
}
void Destroy(Node* root)
{
if (root == nullptr)return;
Destroy(root->_left);
Destroy(root->_right);
delete root;
}
四种旋转逻辑(左单旋,右单旋,左右双旋,右左双旋)
左右双旋与右左双旋
我们在AVL树中,之所以要单独实现这个逻辑,是因为双旋之后的平衡因子需要我们另外去控制
但是我们在红黑树中并没有平衡因子这个概念,而且我们颜色的控制还是统一在旋转完之后控制的
所以我们并不需要显示实现双旋,要实现双旋只需要将把两个单旋直接拿来用即可
左单旋

左单旋的代码如下:(解析在代码下面)
//左单旋
void RotateL(Node* parent)
{
///
Node* curR = parent->_right;
Node* curRL = curR->_left;
Node* Parentparent = parent->_parent;
//旋转逻辑
parent->_right = curRL;
curR->_left = parent;
///
//处理parent
if (curRL)curRL->_parent = parent;
parent->_parent = curR;
///
if (!Parentparent)
{
_root = curR;
curR->_parent = nullptr;
}
else
{
if (Parentparent->_left == parent)
Parentparent->_left = curR;
else
Parentparent->_right = curR;
curR->_parent = Parentparent;
}
///
}
这代码其实很简单,不要将其想得太复杂,首先我们是先将三个节点定义了出来
其中第三个是parent的_parent,我们将其命名为Parentparent
然后将curRL变成parent的右节点
然后parent自己成为curR的左节点
因为本来就是curR那边多出来了,我们旋转一下之后,parent下去了,那么自然就平衡了
(上述为第一块代码区域)
而后面的代码就都是在干一件事:找父节点
因为我们的节点是有三个指针的,一个左,一个右,一个parent
我们接下来解决的就是parent指针的指向
首先是curRL,因为我们不确定其是否为空,所以我们需要判断一下,如果存在,再让其的parent指针指向parent
(上述为第二块代码区域)
然后是parent,parent的_parent指针指向的是curR
最后是curR,我们无法确定Parentparent是否为空,因为如果parent节点旋转之前是根节点呢
所以我们就加了一条判断逻辑(如果为空,就直接让curR做根节点,并将curR的parent节点置空)
然后,如果Parentparent不为空,因为Parentparent还指向着parent节点,所以我们就直接判断curR是要链接在Parentparent的左边还是右边
最后让curR的parent指针指向Parentparent
(上述为第三块代码区域)
右单旋
右单旋其实就是左单旋的镜像处理,代码如下:
// 右单旋
void RotateR(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 (parentParent == nullptr)
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (parent == parentParent->_left)
{
parentParent->_left = subL;
}
else
{
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
}
红黑树的插入 Insert
插入节点的颜色
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 每条路径均包含相同数目的黑色结点
这里我们需要明白的一个点是,我们插入的节点是要什么颜色的
颜色就分为红色和黑色
如果我们插入黑色呢?
只要我们插入黑色,那就必须要调整,因为规则 4 是:每条路径上的黑色节点数量都需要相同
但是我们插入了黑色节点之后,就破坏了规则 4 ,所以就必须要进行处理
但是如果我们插入的是一个红色节点
当我们为红色节点的时候,如果父节点为黑色,那么我们就可以直接插入,并不需要做任何调整,因为并没有违反任何一条规则
但是如果我们的父节点是红色,那就违反规则 3 了,这时候才需要做出相应的调整
我们试想一下,插入黑色节点,就必须要调整,但是如果我们插入红色节点的话,如果父节点也是红才需要调整
就像是你考差了,你拿给爸爸那就肯定要挨顿打,但是如果拿给妈妈看,说不定妈妈心情好,就不会打,说不定看你这么差,就打你一顿
既然选爸爸一定会,选妈妈可能会,那我们肯定还是选妈妈嘛,说不定呢
综上,我们插入的节点颜色为红色
红黑树插入的三种情况
- 每个结点不是红色就是黑色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子结点是黑色的
- 每条路径均包含相同数目的黑色结点
由于我们插入的节点为红色,那么就只有当父节点为红色的时候,才会进入调整的逻辑,所以父节点肯定是红色的
根据规定 2,根节点是黑色的,那既然父节点为红,那上面就一定还有一个爷爷(grandpa)节点(黑色)
那现在就是爷爷节点为黑,父节点和新插入节点为红,就只有一个叔叔(uncle)节点是不确定的

目前情况如上
而叔叔节点分三种情况:为红色、为黑、不存在
1. 叔叔节点为红
如果叔叔节点为红色的话,那就意味着,我们是不需要旋转控制的,因为如果叔叔为黑,父亲和新插入节点都为红,那么父亲和新节点下面就必然有黑色节点,所以新插入的节点其实并不是新插入的,而是其他情况变过来的
但是如果叔叔也为红,那就不会有这种情况,我们只需要变色处理即可
具体就是将父节点和叔叔节点变黑,然后新插入节点还是红色,爷爷节点变红即可

2. 叔叔节点不存在
如果叔叔节点不存在的话,那我们就必须要旋转了

这种情况就是旋转 + 变色
3. 叔叔节点为黑
叔叔节点为黑的话,那也是必须要旋转的
而且,这种情况一定是上面的情况变过来的,因为如果叔叔为黑,那么父节点和新插入节点为红,就不满足每条路径都只有一个黑色节点的规则,所以,父节点的另一个孩子节点一定为黑,并且新插入节点也一定有两个孩子节点且为黑
但是这时候新插入节点和父节点都为红色啊,所以就一定是其他情况变过来的

代码编写
关于红黑树的插入,我们在插入之前,需要先判断一下根节点是否为空,如果为空,就证明这是一棵空树,我们直接new一个节点给根,然后退出即可
接着需要查找一下这个值在不在红黑树里面,如果在的话,我们就没有插入的必要了
bool Insert(const pair<K, V>& data)
{
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = BLACK;
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_data.first < data.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_data.first > data.first)
{
parent = cur;
cur = cur->_left;
}
else
return false;
}
如果能走到下面的逻辑,就证明我们已经找到待插入的位置了,我们只需要做三件事:
- new一个新节点
- 将这个节点的_parent指针与parent的孩子指针处理好
- 新节点颜色设置为红色
cur = new Node(data);
cur->_col = RED;
if (cur->_data.first < parent->_data.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
接着就是颜色改变与旋转的逻辑了
首先,我们的父节点为黑色的话,是不需要进入这个逻辑的,直接插入节点就好,不需要任何调整
然后当我们进入循环,代表着父节点一定为红
这时候就要开始分情况了,叔叔节点在爷爷节点的左边、叔叔节点在爷爷节点的右边
这是两种情况,代表着(右单旋、左右双旋)和(左单旋、右左双旋)
接着我们就是进入判断叔叔的环节了,我们先把代码实现一下:
while (parent && parent->_col == RED)
{
Node* grandpa = parent->_parent;
if (grandpa->_left == parent)
{
// parent 在爷爷节点左边
}
else
{
// parent 在爷爷节点右边
}
}
然后叔叔又分三种情况:
- 叔叔为红
- 叔叔不存在
- 叔叔为黑
这里的话,叔叔为黑与不存在可以当成是一种情况,因为这两种情况都是要旋转
并且不管叔叔在不在,最后变色都不需要叔叔来变色


我们看上图,两种情况都是爷爷变红,父节点变黑
而同时,也还有另一种情况,就是双旋的问题,这个其实比较简单,就是新插入节点在父节点的右或左的问题,如果父节点在爷爷的左,新插入节点在父亲的左,那就是右单旋
如果新插入节点在父亲的右,那就是左右双旋,父节点在爷爷右的情况镜像处理即可
这两个唯一的区别就在于:
双旋是新插入节点变黑,爷爷变红
单旋是父节点变黑,爷爷变红
代码如下:
while (parent && parent->_col == RED)
{
Node* grandpa = parent->_parent;
if (grandpa->_left == parent)
{
// parent 在爷爷节点左边
Node* uncle = grandpa->_right;
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandpa->_col = RED;
cur = grandpa;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
// 单旋的逻辑
RotateR(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
else
{
// 双旋的逻辑
RotateL(parent);
RotateR(grandpa);
cur->_col = BLACK;
grandpa->_col = RED;
}
break;
}
}
else
{
// parent 在爷爷节点右边
Node* uncle = grandpa->_left;
if (uncle && uncle->_col == RED)
{
grandpa->_col = RED;
uncle->_col = parent->_col = BLACK;
cur = grandpa;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
RotateL(grandpa);
grandpa->_col = RED;
parent->_col = BLACK;
}
else
{
RotateR(parent);
RotateL(grandpa);
grandpa->_col = RED;
cur->_col = BLACK;
}
break;
}
}
}
当我们颜色调节和旋转的逻辑处理好之后,我们只需要再让根节点变成黑色即可
因为我们在颜色调节过程中可能会将根节点调整成红色的,比如情况一
这时候我们以防万一,就需要加上这一步,最后return true即可
_root->_col = BLACK;
return true;
插入完整代码
bool Insert(const pair<K, V>& data)
{
if (_root == nullptr)
{
_root = new Node(data);
_root->_col = BLACK;
return true;
}
Node* cur = _root;
Node* parent = nullptr;
while (cur)
{
if (cur->_data.first < data.first)
{
parent = cur;
cur = cur->_right;
}
else if (cur->_data.first > data.first)
{
parent = cur;
cur = cur->_left;
}
else
return false;
}
cur = new Node(data);
cur->_col = RED;
if (cur->_data.first < parent->_data.first)
parent->_left = cur;
else
parent->_right = cur;
cur->_parent = parent;
Node* newone = cur;// 提前记录值以便返回
while (parent && parent->_col == RED)
{
Node* grandpa = parent->_parent;
if (grandpa->_left == parent)
{
// parent 在爷爷节点左边
Node* uncle = grandpa->_right;
if (uncle && uncle->_col == RED)
{
uncle->_col = parent->_col = BLACK;
grandpa->_col = RED;
cur = grandpa;
parent = cur->_parent;
}
else
{
if (cur == parent->_left)
{
// 单旋的逻辑
RotateR(grandpa);
parent->_col = BLACK;
grandpa->_col = RED;
}
else
{
// 双旋的逻辑
RotateL(parent);
RotateR(grandpa);
cur->_col = BLACK;
grandpa->_col = RED;
}
break;
}
}
else
{
// parent 在爷爷节点右边
Node* uncle = grandpa->_left;
if (uncle && uncle->_col == RED)
{
grandpa->_col = RED;
uncle->_col = parent->_col = BLACK;
cur = grandpa;
parent = cur->_parent;
}
else
{
if (cur == parent->_right)
{
RotateL(grandpa);
grandpa->_col = RED;
parent->_col = BLACK;
}
else
{
RotateR(parent);
RotateL(grandpa);
grandpa->_col = RED;
cur->_col = BLACK;
}
break;
}
}
}
_root->_col = BLACK;
return true;
}
红黑树的删除 Erase
红黑树的删除相对来说会更为复杂,因为相比于插入的逻辑,删除会多出更多情况
1. 当删除节点有两个孩子
当删除的节点有两个孩子的时候,我们就需要转换问题:
首先我们需要找到左子树的最右节点或者右子树的最左节点,这里我们以右子树的最左节点为例
然后我们让右子树最左节点将待删除节点的数据覆盖,然后问题就转换成了删除右子树的最左节点
我们之所以要找右子树的最左,是因为这个节点最符合比待删除节点的左子树大,比待删除节点的右子树小,所以我们选择这个节点覆盖待删除节点的数据,然后删除右子树最左节点
而且由于红黑树的特殊性,右子树的最左节点只能是叶子节点,或者只有一个孩子节点且这个孩子节点为叶子节点
试想一下,已经是最左节点了,就不可能有左孩子,那就是右孩子可能在或可能不在
如果不在,那最左节点就是叶子节点,如果右右子树,那么最左节点只能是黑色且这个孩子节点只能是红色,因为如果孩子节点为黑色,那么左边那条路径的黑色节点数量为1,右边的为2,数量不相等,不符合规矩,所以是不可能存在的
综上,如果有一个孩子节点的话,那么这个孩子节点只能是红色的,如果孩子为红色的,那么父亲就一定是黑色的
所以,我们就将问题由删除一个有两个孩子节点的节点,转换成了删除一个有一个孩子或者没有孩子节点的节点,也就是转换成了我们下面讨论的两种大情况
2. 当删除节点有一个孩子
如上所述,如果删除节点只有一个孩子节点的话,那么孩子一定为红,父亲一定为黑
面对这种情况,我们只需要将孩子节点的值赋给父亲,然后变成删除孩子节点
并且,如果我们单单是删除一个红色叶子节点的话,是不需要做任何处理的,直接删除即可,因为删除一个红色节点,不会影响任何一条规则,高度差也不会变成非旋转不可的情况,所以直接删除即可
总结,如果待删除节点只有一个孩子的话,我们只需要将孩子节点的值赋值给父节点,然后将问题转换成删除孩子节点即可
3. 当删除节点没有孩子(叶子节点)
3.1 该节点为红色节点
这种情况直接将该节点删除即可,不需要做任何处理
3.2 该节点为黑色节点
既然这个节点为黑色,那么我们就需要去看这个兄弟节点,因为父节点是什么颜色并无所谓,在后面的逻辑里,父节点的颜色只需要和旋转到最上面位置的节点进行覆盖即可
3.2.1 兄弟节点为红色
既然兄弟为红色,那么父亲就一定为黑色,但是此时我们的待删除节点为黑色,这时,待删除节点路径就有两个黑色节点,兄弟的那条路径只有一个,那就意味着兄弟节点一定有两个孩子,且这两个孩子都为黑

面对这种情况,我们要做的只是旋转 + 变色,并且单旋足矣
代码如下(下图中的uncle就是上文中的uncle节点,这里写错了):
// uncle节点的颜色为红
if (parent->_left == uncle)
{
uncle->_col = BLACK;
uncle->_right->_col = RED;
RotateR(parent);
}
else
{
uncle->_col = BLACK;
uncle->_left->_col = RED;
RotateL(parent);
}
3.2.2 兄弟节点为黑色
如果是兄弟节点为黑色的情况,我们就需要讨论兄弟节点有没有孩子,因为待删除节点也是黑色的,所以删除之后路径上的黑色节点就少了一个,这时候如果兄弟节点有孩子节点的话,我们就能在旋转后通过染色来控制黑色节点的数量
并且由于待删除节点是叶子节点且为黑,所以兄弟节点的孩子必定为红,因为为黑的话路径上的黑色节点数量就不一样了
接下来要分兄弟节点有一个孩子,两个孩子,没有孩子三种情况讨论
3.2.2.1 兄弟节点有一个红色孩子节点
这时候我们就需要看,这个红色孩子节点在兄弟节点的左还是右了
如果兄弟节点在父节点的左,并且兄弟节点的孩子节点在兄弟节点的左,那么我们就右单旋+变色即可,但是这里的变色要注意,我们删除了节点 + 旋转之后,单从结果来看是兄弟节点在最上面,然后父节点下来了,那我们父节点的颜色是不知道的,所以我们只能让兄弟节点的颜色变成父节点的颜色,然后父节点和兄弟节点的孩子节点都变成黑色

如果兄弟节点(按上图)的孩子节点在右的话,就直接走双旋逻辑即可,只不过变色变成了兄弟节点的孩子节点变成父节点的颜色,然后兄弟节点和父节点变黑而已
3.2.2.2 兄弟节点有两个红色孩子节点
面对这种情况,直接单旋即可,变色的逻辑和上面只有一个子节点且同向的处理一样
3.2.2.3 兄弟节点没有孩子节点
如果没有孩子节点,那就意味着,我们在删除完节点之后,没有多余的节点变色使每条路径上的黑色节点与原来保持一致
这时候就得看父节点了,所以我们还得再分两种情况,一种是父节点为红,一种是父节点为黑
3.2.2.3.1 父节点为红
如果此时父节点为红的话,那么就是原本为每条路径都只有一个黑色节点,这时候删除了一个,要想保持原本的黑色节点的数量的话,这时我们可以将父节点染黑,然后兄弟节点变红即可,这样这棵子树的每条路径上的黑色节点就都还是一个
总结,父亲变黑,兄弟变红
3.2.2.3.2 父节点为黑
如果是这种情况的话,我们的黑色节点的数量就没法保持两个了,所以我们就只能将兄弟节点变成红色,然后递归向上调整
直到碰到红色的父节点,节点的黑色路径就能调整回来了
代码如下(下图中的uncle就是上文中的uncle节点,这里写错了):
bool erase(const K& data)
{
if (!_root)return false;
Node* del = _root;
while (del)
{
if (data < del->_data.first)
del = del->_left;
else if (data > del->_data.first)
del = del->_right;
else
break;
}
if (del == nullptr)
return false;
if (_root == del && del->_left == nullptr && del->_right == nullptr)
{
delete _root;
_root = nullptr;
return true;
}
// 有两个孩子的情况
if (del->_left && del->_right)
{
Node* rightMin = del->_right;
while (rightMin->_left)
{
rightMin = rightMin->_left;
}
del->_data = rightMin->_data;
// 将问题转换成叶子节点或者一个节点的删除
del = rightMin;
}
// 有一个孩子的情况
if ((del->_left && !del->_right) || (!del->_left && del->_right))
{
// 一个孩子的情况只能是父黑子红
// 不然不满足每条路径都只有一个黑色节点
Node* child = del->_left == nullptr ? del->_right : del->_left;
del->_data = child->_data;
del = child;
}
/ 没有孩子的情况分为红色和黑色两种
/ 红色直接删,黑色需要细分情况
if (del->_col == BLACK)
{
adjustRBTreeBalance(del);
}
Node* parent = del->_parent;
if (parent->_left == del)
parent->_left = nullptr;
else
parent->_right = nullptr;
delete del;
return true;
}
void adjustRBTreeBalance(Node* del)
{
// 前面已经判断过了,但是这里是后面代码的递归出口
// 因为后面写到兄弟为黑并没有子节点,并且父亲为黑的情况的时候,就需要递归处理
if (_root == nullptr)
return;
Node* parent = del->_parent;
Node* uncle = del == parent->_left ? parent->_right : parent->_left;
if (uncle->_col == BLACK)
{
int type = whichtype(uncle);
switch (type)
{
case LL:
uncle->_col = parent->_col;
parent->_col = uncle->_right->_col = BLACK;
RotateL(parent);
break;
case RR:
uncle->_col = parent->_col;
parent->_col = uncle->_left->_col = BLACK;
RotateR(parent);
break;
case LR:
uncle->_right->_col = parent->_col;
parent->_col = BLACK;
RotateL(uncle);
RotateR(parent);
break;
case RL:
uncle->_left->_col = parent->_col;
parent->_col = BLACK;
RotateR(uncle);
RotateL(parent);
break;
default:
// 没有红色子节点
// 需要判断父节点是什么颜色
// 如果父节点为红色,交换颜色即可
// 如果父节点为黑色, 则uncle节点变红后递归处理
if (parent->_col == RED)
{
parent->_col = BLACK;
uncle->_col = RED;
}
else
{
uncle->_col = RED;
adjustRBTreeBalance(parent);
}
break;
}
}
else
{
// uncle节点的颜色为红
if (parent->_left == uncle)
{
uncle->_col = BLACK;
uncle->_right->_col = RED;
RotateR(parent);
}
else
{
uncle->_col = BLACK;
uncle->_left->_col = RED;
RotateL(parent);
}
}
}
int whichtype(Node* uncle)
{
Node* parent = uncle->_parent;
if (uncle == parent->_left)
{
if (uncle->_left != nullptr)
{
return RR;
}
else if (uncle->_right != nullptr)
{
return LR;
}
}
else
{
if (uncle->_right != nullptr)
{
return LL;
}
else if (uncle->_left != nullptr)
{
return RL;
}
}
// 无红色节点的情况
return 0;
}
结语
看到这里,这篇博客有关红黑树的相关内容就讲完啦~( ̄▽ ̄)~*
如果觉得对你有帮助的话,希望可以多多支持博主喔(○` 3′○)
3056

被折叠的 条评论
为什么被折叠?



