在二叉搜索树中,基本操作如结点的插入、删除、查找的性能上界都得不到保证,原因在于二叉搜索树的构造依赖于其结点值的插入顺序,最坏情况下二叉搜索树会退化为单链表(如下图所示)。因此我们需要对二叉搜索树做出一些改进和限制,从而使其拥有更好更稳定的性能上界保证。红黑树就是一种自平衡的二叉搜索树。
定义
红黑树除了满足二叉搜索树的性质以外,还有以下性质:
(1)红黑树的结点都是带有“颜色”属性的,要么是红色要么是黑色。
(2)根结点是黑色的。
(3)所有叶结点的子结点(空)都是黑色的。
(4)红色结点的两个结点都是黑色结点(即不存在在同一条连接上相邻的两个红色结点)。
(5)红黑树是完美黑色平衡的,即任意空结点到根结点的路径上的黑色结点数量相同。
红黑树(其中的NIL是实际上不存在的结点,即空结点)
红黑树其实是2-3树(又称2-3-4树)的等同,即任何一颗红黑树都可以化为2-3树,反之也成立。因此红黑树既是二叉搜索树,也是2-3树。在 2-3-4 树上的插入和删除操作也等同于在红黑树中颜色变换和结点旋转。这使得 2-3-4 树成为理解红黑树背后的逻辑的重要工具。
我们定义一个红黑树的结点如下:
using RED = true;
using BLACK = false;
struct RBT_Node {
int val = 0; //结点值
RBT_Node *parent = nullptr; //指向当前结点的父结点
RBT_Node *left = nullptr, *right = nullptr; //left指向当前结点的左子结点,right指向当前结点的右子结点
bool color = RED; //结点颜色
RBT_Node(int x) : val(x), parent(nullptr), left(nullptr), right(nullptr), color(RED);
};
旋转
红黑树的结点搜索与二叉搜索树一致,但结点的插入和删除要复杂得多,因为红黑树相对二叉搜索树添加了许多限制,而插入和删除会使得红黑树的性质被破坏,因此我们需要进行一定的结点颜色变换和旋转来恢复红黑树的性质。
在写红黑树的结点插入和删除的代码之前,需要定义结点旋转的操作。旋转分为左旋和右旋,假设一个结点为x,左旋操作定义为将x“逆时针旋转”(实际操作是改变指针指向)使得x的右子结点成为x的父结点,而x的右子结点的左子树成为x的右子树;右旋操作定义为将x“顺时针旋转”(实际操作是改变指针指向)使得x的左子结点成为x的父结点,而x的左子结点的右子树成为x的左子树(左旋与右旋方向相反)。如下图所示:
左旋操作:
void rotateLeft(RBT_Node *node, RBT_Node *root) { //node为旋转中心
RBT_Node *right = node->right, *parent = node->parent;
if ((node->right = right->left) != nullptr)
right->left->parent = node;
right->left = node;
right->parent = node->parent;
if (node->parent == nullptr) { //若parent为空则说明红黑树为空,需将node设为根结点
root = node;
return ;
}
if (node == node->parent->left)
node->parent->left = right;
else
node->parent->right = right;
node->parent = right;
}
右旋操作
void rotateRight(RBT_Node* node, RBT_Node *root) {
RBT_Node *left = node->left;
if ((node->left = left->right) != nullptr)
left->right->parent = node;
left->right = node;
left->parent = node->parent;
if (node->parent == nullptr) {
root = node;
return ;
}
if (node == node->parent->left)
node->parent->left = left;
else
node->parent = left;
node->parent = left;
}
插入
红黑树也是二叉搜索树,因此向红黑树中插入结点与二叉搜索树相同,但由于可能会破坏红黑树的性质2和性质4,因此插入后还得根据需要进行旋转、颜色转换等工作以恢复红黑树的性质。需要注意的是,插入结点默认为红色结点,因为插入红色结点可能破坏红黑树的性质4,而插入黑色结点可能会破坏性质2、3、5。我们需要为后续的修复工作减少难度。
void insertRBT_Node(RBT_Node *node, RBT_Node *root) {
if (root == nullptr) {
root = node;
return ;
}
RBT_Node* parent = nullptr, cur = root;
while (cur != nullptr) {
parent = cur;
if (cur->val < node->val)
cur = cur->right;
else if (cur->val > node->val)
cur = cur->left;
else
return nullptr;
}
node->parent = parent;
if (parent->val < node->val)
parent->right = node;
else
parent->left = node;
node->color = RED;
insertFixUp(node, root);
}
从上面的插入函数源码中可以看到,除了恢复红黑树性质的insertFixUp以外,插入函数与二叉搜索树的插入大致相同。insertFixUp函数的主要工作是将被破坏的红黑树恢复,根据插入结点(也是当前结点)的父结点的性质,分为以下3种情况:
(1)当前结点没有父结点,即该插入结点为根结点 。这种情况只需要将插入结点的颜色改为黑色即可。
(2)当前结点的父结点是黑色结点。此时红黑树的性质并没有被破坏,因此不需要操作。
(3)当前结点的父结点是红色结点。此时破坏了红黑树的性质4和性质5,修改操作较为复杂。由于当前结点的父结点是红色结点,那么当前结点一定存在祖父结点(父结点的父结点),进一步讲一定存在叔叔结点(即使为空也视为黑色结点)。因此按照叔叔结点属性的不同,修复操作也分为以下3种情况:
a.叔叔结点是红色。这种情况的处理策略是:将父结点改为黑色;将叔叔结点改为黑色;将祖父结点改为红色,并将其设为当前结点,对其进行修复操作。
如上图所示,由于插入破坏了红黑树的性质4,因此我们需要改变某些结点的颜色属性。假设将插入结点改为黑色,反而破坏了红黑树的更多性质,因此应该修改其他结点。为了修复性质4,插入结点又要维持红色,那么只能将其父结点改为黑色,那么祖父结点的颜色应改为红色,对应的叔叔结点应改为黑色。这样在任何一条从叶结点到根节点的路径上的黑色结点数目一增一减,最后没有变化。然后我们需要对祖父结点进行相同的操作。
b.叔叔结点是黑色,且当前结点是其父结点的右子结点。此时应该以父结点为支点进行左旋操作。
这样操作的本质是将状态转化为a再进行操作。
c.叔叔结点是黑色,且当前结点是其父结点的左子结点。此时应该将父结点和祖父结点都改为黑色结点,并以祖父结点为支点对其进行右旋操作。
操作目的是将状态转化为b
综上,修复函数的源码如下:
void insertFixUp(RBT_Node *node, RBT_Node *root) {
RBT_Node *parent, *gparent;
while ((parent = node->parent) != nullptr && parent->color == RED) {
gparent = parent->parent;
if (gparent->left == parent) {
if (gparent->right != nullptr && gparent->right->color == RED) {
parent->color = BLACK;
gparent->right->color = BLACK;
gparent->color = RED;
node = gparent;
continue;
}
if (node == parent->right)
rotateLeft(parent, root);
else {
parent->color = BLACK;
parent->parent->color = BLACK;
rotateRight(parent->parent, root);
}
}
else {
if (gparent->left != nullptr && gparent->left->color == RED) {
parent->color = BLACK;
gparent->left->color = BLACK;
gparent->color = RED;
node = gparent;
continue;
}
if (node == parent->right)
rotateLeft(parent->parent, root);
else {
parent->color = BLACK;
parent->parent->color = BLACK;
rotateRight(parent->parent, root);
}
}
}
root->color = BLACK; //若parent不为空但它是黑色结点时,这一行并没有实际意义
}
删除
红黑树的结点删除首先也是按照二叉搜索树的结点删除步骤一样删除结点,然后再修复红黑树。 但如果待删除结点是黑色结点,会破坏红黑树的性质5,因此对于删除黑色结点的情况要进行修复。
void deleteRBT_Node(RBT_Node *root, RBT_Node *node) {
RBT_Node *parent, *child;
bool color;
if (node->left != nullptr && node->right != nullptr) { //待删除结点左右子树都不为空
RBT_Node *del = node->right;
while (del->left != nullptr) //寻找后继结点
del = del->left;
node->val = del->val;
node->color = del->color;
if (del->parent == node)
node->right = del->right;
parent = del->parent;
child = del->right;
parent->left = child;
child->parent = parent;
if (del->color == BLACK)
removeFixUp(child, parent, root);
delete del;
return ;
}
if (node->left == nullptr)
child = node->right;
else
child = node->left;
parent = node->parent;
if (child != nullptr)
child->parent = parent;
if (parent != nullptr) {
if (parent->left == node)
parent->left = child;
else
parent->right = child;
}
else
root = child;
if (node->color == BLACK)
removeFixUp(child, parent, root);
delete node;
}
由于删除结点可能会破坏红黑树的性质2、4和5,因此我们在修复函数removeFixUp中要进行针对性修复。
当我们删除一个红色结点后并不会破坏红黑树的性质,只有删除黑色结点后导致一条或多条叶结点到根结点的路径中少了一个黑色结点。删除结点x后,其后继结点y取代了原结点的位置,结点y可能是红色结点也可能是黑色结点,假设给结点y再添加一个颜色属性,让其变成“黑+黑”或“红+黑”结点,就可以弥补前面所说的删除结点后导致的路径黑色结点数目不同的问题。removeFixUp修复函数的核心思想就是将这一个“额外的”黑色属性不断沿根结点移动,直到遇到下面几种情况:
1.当前结点是“红+黑”结点,那么直接将当前结点变为黑色结点即可。
2.当前结点是“黑+黑”结点,且当前结点是根结点,这时不需要做任何修复。
3.当前结点是“黑+黑”结点,且当前结点不是根结点,这种情况较为复杂,可以再细分为4个子情况:
a)当前结点的兄弟结点是红色(意味着父结点和当前结点的子结点都是黑色)。这种情况的处理策略为:将当前结点的兄弟结点改为黑色;将当前结点的兄弟结点改为黑色;以当前结点的父结点作为支点进行左旋;左旋后重新设置当前结点的兄弟结点,再回到循环开始进行修复。
b)当前结点的兄弟结点是黑色,且该兄弟结点的左右子结点都是黑色。此时只需要将兄弟结点变为红色,并让当前结点的父结点成为新的“当前结点”。
c)当前结点的兄弟结点为黑色,且兄弟结点的左子结点是红色,右子结点是黑色。此时的处理策略为:将兄弟结点的左子结点设为黑色;将兄弟结点设为红色;以兄弟结点为支点进行右旋;右旋后重新设置当前结点的兄弟结点。
d)当前结点的兄弟结点为黑色,且兄弟结点的右子结点是红色,左子结点是任意颜色。处理策略为:将当前结点的父结点颜色赋值给当前结点的兄弟结点;将父结点设为黑色;将兄弟结点的右子结点设为黑色;对当前结点的父结点进行左旋;左旋后设置当前结点为根节点。
void removeFixUp(RBT_Node *node, RBT_Node *parent, RBT_Node *root) {
RBT_Node *other;
while ((node == nullptr || node->color == BLACK) && node != root) {
if (parent->left == node) {
other = parent->right;
if (other->color == RED) {
other->color = BLACK;
other->parent->color = RED;
rotateLeft(parent, root);
other = parent->right;
}
if ((other->left == nullptr || other->left->color == BLACK) && (other->right == nullptr || other->right->color == BLACK)) {
other->color = RED;
node = other;
parent = node->parent;
}
else {
if (other->right == nullptr || other->right->color == BLACK) {
other->left->color = BLACK;
other->color = RED;
rotateRight(other, root);
other = parent->right;
}
other->color = parent->color;
parent->color = BLACK;
other->right->color = BLACK;
rotateLeft(parent, root);
node = root;
break;
}
}
else {
other = parent->left;
if (other->color == RED) {
other->color = BLACK;
parent->color->color = RED;
rotateRight(parent, root);
other = parent->left;
}
if ((other->left == nullptr || other->left->color == BLACK) && (other->right == nullptr || other->right->color == BLACK)) {
other->color = RED;
node = parent;
parent = node->parent;
}
else {
if (other->left == nullptr || other->left->color == BLACK) {
other->right->color = BLACK;
other->color = RED;
rotateLeft(other, root);
other = parent->left;
}
other->color = parent->color;
parent->color = BLACK;
other->left->color = BLACK;
rotateRight(parent, root);
node = root;
break;
}
}
}
if (node != nullptr)
node->color = BLACK;
}
本文文字内容参考《算法导论(第3版)》及skywang12345的个人博客。
本文源码借鉴于Linux内核(有改动) ,如有疏漏仅为个人失误。