红黑树的概述:
平衡树的搜索效率最高,但是由于平衡因子的限制,如果是进行插入和删除操作时,没有达到左右子树高度相差不超过1这个平衡条件,就会执行旋转操作使其达到平衡状态,频繁的调整会使其性能下降,那么如何弱化平衡条件,使其不在频繁的调整就可以很好的达到我们所需要的平衡呢?为了实现这一目的,红黑树应运而生。红黑树是一种二叉搜索树,因此它也满足二叉搜索的特征:任意节点所包含的值,大于左孩子的值,小于有孩子的值。不仅如此,它还有一些其他的特征。
红黑树的特性:
1.每个节点是红色或是黑色。
2.根节点是黑色。
3.每个叶子节点是黑色(这里指的是为空的叶子节点)
4.红色节点的左右孩子为黑色(不能出现两个连续的红色节点)。
5.任一节点到叶所有路径黑节点数量相同
口诀:左根右 根叶黑 不红红 黑路同
引出的结论:最长路径不超过最短路径的两倍(AVL左右高度不会超过1,由于AVL树比较严格,因此插入删除调整更加频繁)
因此AVL树在查询上面更高效,红黑树在插入删除上面更高效
红黑树的插入:
根据红黑树的五条性质,那么我们应该如何正确的插入节点。首先是要需明确的是插入节点的颜色。如果插入节点的颜色是黑色,那么就会违反黑路同的性质,调整起来比较麻烦,但如果插入的是红色节点,有可能不会破坏红黑树的性质,有可能破坏(这时两个红色相邻,或者违反根叶黑),相对于黑色节点来说,插入红色影响更小,因此红黑树插入节点默认红色。
现在我们考虑插入位置:
插入位置为根节点,直接变为黑色。
插入位置的叔叔是红色,叔父爷变色,爷爷变插入节点,看是否违反红黑树性质。
插入节点的叔叔是黑色,按照(LL,RR,LR,RL)进行旋转,然后变色
左右旋转:
左右旋转的代码如下:
/* 将x进行左旋,将左、右、父节点进行更新
* px px
* | |
* x y
* / \ --(左旋)--> / \
* lx y x ry
* / \ / \
* ly ry lx ly
*/
static void leftRotate(RBTree *tree, RBNode *x) {
RBNode *y = x->right;
x->right = y->left;
if (y->left) {
y->left->parent = x;
} // 将x与ly接上
y->parent = x->parent; // 将x的位置异位给y
if (x->parent) {
if (x->parent->left == x) {
x->parent->left = y;
} else {
x->parent->right = y; // px和y接上
}
} else {
tree->root = y; // 否则顶点为y
}
y->left = x;
x->parent = y;
}
/* 将y进行左旋,将左、右、父节点进行更新
* py py
* | |
* y x
* / \ --(右旋)--> / \
* x ry lx y
* / \ / \
* lx rx rx ry
* */
static void rightRotate(RBTree* tree, RBNode *y) {
RBNode *x = y->left;
y->left = x->right;
if (x->right) {
x->right->parent = y;
}
x->parent = y->parent;
if (y->parent) {
if (y->parent->right == y) {
y->parent->right = x;
} else {
y->parent->left =x;
}
} else {
tree->root = x;
}
x->right = y;
y->parent = x;
}
LL型:插入的节点在开始不平衡的点(这里指的是根节点)的左侧,而且在父节点的左侧 -- 直接进行右旋,旋转完成后对旋转中心点和旋转点进行变色
RR型:插入的节点在开始不平衡的点(这里指的是根节点)的右侧,而且在父节点的右侧 -- 直接进行左旋,旋转完成后对旋转中心点和旋转点进行变色
LR型:插入节点在开始不平衡的点(这里指的是根节点)左侧,在父节点的右侧,先左旋左孩子,再右旋“根节点”,旋转完成后对旋转中心点和旋转点进行变色
RL型:插入节点在开始不平衡的点(这里指的是根节点)右侧,在父节点的左侧,先右旋右孩子,再左旋“根节点”,旋转完成后对旋转中心点和旋转点进行变色
以 17 18 23 34 27 15 9 6 8 5 25为例构建一棵红黑树,为方便省略掉空节点(暗红是新插入的节点)
红黑树插入调整代码:
static void insertFixUp(RBTree *tree, RBNode *node) {
RBNode *parent = NULL, *grandParent = NULL;
RBNode *uncle = NULL;
RBNode *tmp;
parent = node->parent;
// 判断父节点是否为红色,父节点是黑色直接插入不需要调整
while (parent && parent->color == RED) {
// 找祖父节点 -- 为了找叔叔
grandParent = parent->parent;
if (parent == grandParent->left) {
uncle = grandParent->right;
} else {
uncle = grandParent->left;
}
if (uncle && uncle->color == RED) { // 叔叔是红色
uncle->color = BLACK;
parent->color = BLACK;
grandParent->color = RED; // 叔父爷变色 + cur换指向
node = grandParent;
parent = node->parent; // 将父节点指向当前节点的父节点,因为当前结点变成了grandParent
continue;
}
// 叔叔是黑色
if (grandParent->left == parent) { // L,左边为父节点
if (parent->right == node) { // R
leftRotate(tree, parent); // 左旋父节点
tmp = parent; // 交换父节点和插入的 node 节点
parent = node;
node = tmp;
}
// LL
rightRotate(tree, grandParent); // 旋转爷爷,旋转点和旋转中心的进行变色(旋转点是爷爷节点,旋转中心点是父节点)
grandParent->color = RED;
parent->color = BLACK;
} else { // 同上
if (parent->left == node) { // RL
rightRotate(tree, parent);
tmp = parent;
parent = node;
node = tmp;
} // RR
leftRotate(tree, grandParent);
grandParent->color = RED;
parent->color = BLACK;
}
}
tree->root->color = BLACK; // 将根节点置为黑
}
红黑树的删除:
对于AVL树来说:
删除的是叶节点,直接删除;
删除的是只有左/右子树的节点,直接让左/右子树代替即可
删除的节点有左右子树,找到前驱(左子树最大的)或者后继(右子树最小的),用它代替删除的节点,删除前驱/后继
对于红黑树也相同,如果按照上述方法删除时破坏了红黑树的性质,直接调整即可,由于第三种情况会转化为第一和第二种情况,那么讨论第一和第二种情况即可。
对于只有一个(平衡状态)子树来说:只能是一黑一红,而且黑色节点是红色节点的父节点。为什么呢?
如果是一红一黑,无论是在左还是有右子树上加一个黑色节点,都会违反黑路同的性质。
因此可以引出删除的套路:
1.只有左右孩子,直接代替后变黑
2.没有孩子
2.1红节点:删除后不做任何调整(因为不破坏红黑树的性质)
2.2黑节点(一定会破坏黑路同)
2.2.1兄弟是黑色
1)兄弟至少有一个红孩子(根据LL,LR,RL,RR变色+旋转)
2)兄弟孩子都是黑色(兄弟变红,双黑上移)
2.2.2兄弟是红色:兄父变色,朝双黑旋转
为了好理解,将删除的黑色节点变成"双黑节点",要做的就是将双黑变为单黑
2.2.1.1兄弟至少一个红孩子
1)LL 变色(r变成s的颜色,s变成p的颜色,p变成黑色),如果有两个红色节点,将其归为LL,完成变色后进行右转,如果不违反了黑路同,将双黑变为单黑
2)RR变色(与LL同理),变色后进行左旋,如果是两个红色节点,归为RR
3)LR变色,将r变为p的颜色,p变黑,之后进行旋转
4)RL变色同理
2.2.1.1兄弟孩子都是黑色节点
将兄弟节点变为了红色,满足这一条路径的黑路同的性质,但是可能不满足从根节点出发黑路同的性质,因此将双黑节点进行上移(移到删除节点的父节点上),如果父节点是黑色,此时就有可能转化为兄弟节点至少一个孩子的情况,根据上面进行调整
如果是双黑移动到的节点是红色,直接变成单黑
当双黑遇到根节点,直接变成单黑
2.2.2 删除没有孩子的黑节点,而且兄弟是红色:父兄变色,朝双黑旋转,按照上面的进行调整,此时应该为,兄弟变红,双黑上移,遇到红色变单黑