文章目录:
- 红黑树的介绍
- 红黑树节点定义
- 红黑树的插入操作
- 红黑树的删除
- 红黑树的验证
- 红黑树 vs AVL树
红黑树的介绍
红黑树(Red-Black-Tree),通常写为 R-B Tree。它是一种特殊的二叉搜索树。红黑树的每个节点上都有一个存储位来标识节点的颜色,可以是红(Red)或者黑(Black)。通过对任意一条从根到叶子的路径上各个节点颜色的限制,红黑树确保没有一条路径比其它路径长出两倍,因此是接近平衡的。
红黑树的特性:
- 每个节点只能是黑色或者红色
- 根节点是黑色的
- 如果一个节点是红色的,则它的两个孩子节点必须是黑色的
- 对于每一个节点,从该节点到其它所有后代叶节点的简单路径上均包含相同数目的黑色节点。
- 每一个叶子节点(NIL)是黑色的,注意:这里的叶子节点指的是空节点
❓为什么满足以上性质就能确保树中最长的路径中节点个数不会超过最短路径中节点个数的两倍?
由性质3和性质4可以看出,红黑树中不会出现连续的红节点,且从某一节点到其所有叶子节点路径上包含的黑色节点数目相等。
最短路径:全部由黑色节点组成
最长路径:一黑一红节点组成,红色节点的数量等于黑色节点的数量
红黑树节点定义
这里的红黑树我们实现为 kv 模型,为了便于后续的操作,我们将红黑树定义为三叉链结构。使用枚举的方法标识节点的颜色,增加代码的可读性。
enum Color
{
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;
// 存储节点颜色
Color _col;
RBTreeNode(const pair<K,V>& kv)
:_left(nullptr)
,_right(nullptr)
,_parent(nullptr)
,_kv(kv)
,_col(RED)
{}
};
❓为什么在节点的构造函数中将节点初始化为红色,而不是黑色呢?
- 若新增到红黑树中的节点为红色,此时它的父节点也正好是红色节点,即出现了连续的红色节点,破坏了规则3。若其父节点本身是黑色的,那么我们就不需要进行调整。
- 若新增到红黑树中的节点为黑色,那么插入新增节点的这条路径上就比其它的路径多了一个黑色节点,破坏了规则4。因此我们必须对其进行调节。
总结:插入红色节点可能破坏规则3,插入黑色节点一定破坏规则4。规则4被破坏即影响其它路径,影响面积非常大,若要使其再次满足红黑树的规则将很难进行调节。因此我们插入新的节点都初始化为红色,便于后续调节使其满足红黑树的规则。
红黑树的插入操作
将一个节点插入到红黑树中,需要执行哪些操作步骤呢?
1️⃣ 按照二叉搜索树的规则,将节点插入到对应的位置。
2️⃣ 检测插入新节点之后树是否满足红黑树的规则。
3️⃣ 若插入节点的父节点是红色,则需要对树进行一系列的旋转和变色操作,使其满足红黑树的规则。
🎯新插入的节点颜色默认为红色。因此:若其父节点的颜色是黑色,没有违反红黑树的规则,不需要进行调整;但当插入的节点的父节点颜色是红色时,就违反了规则3不能有连续的红节点。这时需要对红黑树分情况来处理。具体如何对其进行处理,主要看新增节点的叔叔节点(即新增节点父节点的兄弟节点),根据新增节点叔叔颜色的不同,可将红黑树的调整分为三种情况。
注意:插入节点的父节点是红色,说明了其父节点不是根节点,因此插入节点的父节点的父节点(即祖父节点)就一定存在。
规定:cur为新增节点,p为其父节点,g为其祖父节点(即p的父节点),u为叔叔节点。
情况1:新增节点的叔叔存在且为红色;cur为红,p为红,g为黑,u为红 |
情况1的抽象图如下所示:
插入 cur 节点之后,出现了连续的红色节点,因此我们对此进行了处理。将其父节点变为黑色,为了保证每条路径上黑色节点的数量相等,需要将其叔叔节点(u)也变为黑色,然后将祖父节点(g)变为红色。但是调整并没有结束,这里需要对祖父(g)节点进行判断:
若 g 是根节点,调整完成之后,需要将其变为黑色。
若 g 是一棵子树的根节点,需要将祖父当作新插入的节点,继续按照相应的规则向上调整。
叔叔(u)存在且为红时,我们不需要考虑新增节点是父亲的左孩子还是右孩子,因为红黑树以颜色标识节点,所以调整方法是一样的。
情况2:新增节点的叔叔存在且为黑色;cur为红,p为红,g为黑,u为黑 |
这种情况是在情况1向上调整的过程中出现的,cur 不可能是新增节点,而是在情况1调节过程中更新的祖父节点(g)。因为新增节点之前,此二叉搜索树是满足红黑树的规则的,若 cur 是新增节点,那么该树在新增节点之前已经违反了红黑树的规则了(从任意一个节点出发,以它为根节点的所有路径上的黑色节点相等),即路径上黑色节点的数量不相等。因此,cur 不可能是新增节点,
若叔叔(u)存在且为黑色时,仅仅是对节点进行变色已经无法处理了,这时我们需要按照相应的规则对其进行旋转处理。因为涉及到旋转,因此我们需要考虑 cur 是在父节点的左孩子和右孩子两种情况。
✔️若 cur 为 p 的左孩子,需要先进行右单旋,然后进行颜色的调整。调整完成之后这棵子树的根节点变成了黑色,则不需要继续往上进行处理了。
其抽象图如下所示:
处理步骤:
step1:以 g 为旋转点进行右单旋
step2:将 g 变成红色,p 变成黑色
注意:若 g 的右孩子是 p , p的右孩子是 cur 时,这时呈现出的是一条向左倾斜的直线,这是我们先以 p 为旋转点进行左单旋,然后以同样的方式进行颜色的调节。
✔️若 cur 为 p 的右孩子,需要先进行左右双旋,然后进行颜色的调整。调整完成之后这棵子树的根节点变成了黑色,则不需要继续往上进行处理了。
其抽象图如下所示:
处理步骤:
step1:以 p 为旋转点进行左单旋
step2:以 g 为旋转点进行右单旋
step3:将 g 变成红色,cur 变成黑色
注意:若 g 的右孩子是 p , p的左孩子是 cur 时,这时呈现出的是一条形状类似于大于符号的折线,这时我们先以 g 为旋转点进行右单旋,再以 p 为旋转点进行左单旋,然后以同样的方式进行颜色的调节。
情况3:新增节点的叔叔不存在 |
若 u 不存在,则 cur 一定是新增节点,若 cur 不是新增节点,则 cur 和 p 一定有一个节点的颜色是黑色,不满足性质4:每条路径黑色节点个数相同。
✔️若新增节点在 p 的左边,即呈现出来的是一条向右倾斜的直线。这时我们需要对其进行右单旋然后再进行变色处理。
其抽象图如下所示:
处理步骤:
step1:以 g 为旋转点进行右单旋
step2:将 g 变成红色,p 变成黑色
注意:若 g 的右节点为 p , p 的右节点为 cur 。此时呈现出一条向左倾斜的直线,这是我们需要进行左单旋处理,然后调整节点颜色。
✔️若新增节点在 p 的右边,即呈现出来的是一条类似于小于符号的折线。这时我们需要对其进行左右双旋然后再进行变色处理。
处理步骤:
step1:以 p 为旋转点进行左单旋
step2:以 g 为旋转点进行右单旋
step3:将 g 变成红色,cur 变成黑色
注意:若 g 的右节点为 p , p 的左节点为 cur 。这时呈现出的是一条形状类似于大于符号的折线,这是我们先以 p 为旋转点进行右单旋,再以 g 为旋转点进行左单旋,然后以同样的方式进行颜色的调节。
实际上情况三的处理和情况二是一样的
红黑树新增节点代码实现:
pair<Node*, bool> insert(const pair<K, V>& kv)
{
// 一开始插入新节点时树为空,则直接让新节点作为根节点
if (_root == nullptr)
{
_root = new Node(kv);
_root->_col = BLACK;
return make_pair(_root, true);
}
Node* cur = _root;
Node* parent = _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 make_pair(cur, false);
}
// 走到这里就已经找到了将要插入的位置
Node* newnode = new Node(kv);
newnode->_col = RED;
// 判断待插入节点与parent指向值的大小,将待插入节点插入到正确的位置
if (parent->_kv.first < kv.first)
{
parent->_right = newnode;
newnode->_parent = parent;
}
else
{
parent->_left = newnode;
newnode->_parent = parent;
}
cur = newnode; // newnode节点不动,便于后续返回
// 若父亲存在且为红色就需要进行处理
while (parent&& parent->_col == RED)
{
// 若父节点为红色,则祖父节点一定存在,不需要进行判断
Node* grandfather = parent->_parent;
if (parent == grandfather->_left)
{
Node* uncle = grandfather->_right;
//情况1:uncle存在且为红
if (uncle && uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续向上进行处理
cur = grandfather;
parent = cur->_parent;
}
else // 情况2+3:uncle不存在或者uncle存在且为黑
{
// 情况2:右单旋
if (cur == parent->_left)
{
RotateR(grandfather);
grandfather->_col = RED;
parent->_col = BLACK;
}
else // 左右双旋
{
RotateL(parent);
RotateR(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break; // 这两种情况处理完成已经符合红黑树规则了,不需要继续往上处理了
}
}
else // grandfather->_right == parent;
{
Node* uncle = grandfather->_left;
// 情况1
if (uncle&& uncle->_col == RED)
{
parent->_col = uncle->_col = BLACK;
grandfather->_col = RED;
// 继续向上进行处理
cur = grandfather;
parent = cur->_parent;
}
else // 情况2+3
{
if (cur == parent->_righ)
{
RotateL(grandfather);
parent->_col = BLACK;
grandfather->_col = RED;
}
else // cur == parent->_left
{
RotateR(parent);
RotateL(grandfather);
cur->_col = BLACK;
grandfather->_col = RED;
}
break; // 这两种情况处理完成已经符合红黑树规则了,不需要继续往上处理了
}
}
}
// 将根节点的颜色处理为黑色
_root->_col = BLACK;
return make_pair(newnode, true);
}
// 旋转逻辑和AVL树的旋转是一样的
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
Node* parentParent = parent->_parent;
// 让parent的右指针指向subRL,判断一下subRL是否为空
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
// subR的左指针链接parent
subR->_left = parent;
parent->_parent = subR;
// parent为根的情况,更新根节点,让根节点指向空
if (_root == parent)
{
_root = subR;
_root->_parent = nullptr;
}
else //若parent为一棵子树,则链接与parentParent的关系
{
if (parentParent->_right == parent)
parentParent->_right = subR;
else
parentParent->_left = subR;
subR->_parent = parentParent;
}
}
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
Node* parentParent = parent->_parent;
// 将subLR链接到parent的左边,这里注意subLR可能为空的情况,需要判断一下
parent->_left = subLR;
if (subLR)
{
subLR->_parent = parent;
}
// 将parent这棵子树链接到subL的右指针
subL->_right = parent;
parent->_parent = subL;
// 若parent为根,则更新新的根节点
if (parent == _root)
{
_root = subL;
_root->_parent = nullptr;
}
else //若parent为一棵子树,则链接与parentParent的关系
{
if (parentParent->_right == parent)
parentParent->_right = subL;
else
parentParent->_left = subL;
subL->_parent = parentParent;
}
}
红黑树的删除
红黑树的删除情况比较复杂,这里就不做讲解了,具体详解及其实现可参考:红黑树删除
红黑树的验证
红黑树是从二叉搜索树变换过来的,因此它是满足二叉搜索树规则的,因此可检测其是否符合二叉搜索树的规则(中序遍历为有序序列)。然后检测其是否符合红黑树的特性。
1.使用中序遍历去检测其中序遍历是否有序。
void _InOrder(Node* root)
{
if (root == nullptr)
return;
_InOrder(root->_left);
cout << root->_kv.first << " -> " << root->_kv.second << endl;
_InOrder(root->_right);
}
void InOrder()
{
_InOrder(_root);
}
2.检测树中最长路径是否超过最短路径的两倍。
// 计算树中最长路径的长度
int _maxHeight(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = _maxHeight(root->_left);
int rightHeight = _maxHeight(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
// 计算树中最短路径的长度
int _minHeight(Node* root)
{
if (root == nullptr)
return 0;
int leftHeight = _minHeight(root->_left);
int rightHeight = _minHeight(root->_right);
return leftHeight < rightHeight ? leftHeight + 1 : rightHeight + 1;
}
void height()
{
cout << "最长路径:" << _maxHeight(_root) << endl;
cout << "最短路径:" << _minHeight(_root) << endl;
}
3.检测该树是否符合红黑树的性质。
bool _CheckBlance(Node* root, int blackNum, int count)
{
// 走到nullptr之后,判断count和blackNum是否相等
if (root == nullptr)
{
if (count != blackNum)
{
cout << "违反性质4:每条路径上的黑色节点个数必须相同" << endl;
return false;
}
return true;
}
// 检测当前节点与其父节点是否都为红色
if (root->_col == RED && root->_parent && root->_parent->_col == RED)
{
cout << "存在连续红色节点,违反性质3" << endl;
return false;
}
// 统计黑色节点数量
if (root->_col == BLACK)
++count;
return _CheckBlance(root->_left, blackNum, count) && _CheckBlance(root->_right, blackNum, count);
}
bool CheckBlance()
{
// 空树也是红黑树
if (_root == nullptr)
return true;
if (_root->_col == RED)
{
cout << "根节点为红色,违反性质二" << endl;
return false;
}
// 找最左路径黑色节点数量做参考值 - 比较基准值
int blackNum = 0;
Node* left = _root;
while (left)
{
if (left->_col == BLACK)
blackNum++;
left = left->_left;
}
// 检测是否满足红黑树性质,count用来记录路径中黑色节点数量
int count = 0;
return _CheckBlance(_root, blackNum, count);
}
红黑树 vs AVL树
红黑树和AVL树都是高效的平衡二叉树,增删查改的时间复杂度都是O(logN),红黑树是一种弱平衡二叉搜索树,其只需要保证最长路径不超多最短路径的2倍。相对于要求严格的AVL树而言,降低了插入旋转的次数,所以在经常进行增删查改的结构中红黑树比AVL树性能更优,且红黑树的实现相较而言更加简单,所以实际运用中红黑树更加广泛。红黑树广泛用于C++的STL中,map 和 set 都是红黑树实现的。
🌍增删查改的复杂度:
AVL树:可以稳定在O(logN)
红黑树:一般情况下是O(logN),极端情况下是O(log2N)。它们基本上没有差别。