红黑树的存在是为了改变二叉排列树的性能,在最坏的时候,生成的二叉排列树就是链表,这种情况完全没有了二叉排列数的折半查找的优势,时间复杂度变回O(n)
什么是红黑树
红黑树本质上是一种二叉查找树,但它在二叉查找树的基础上额外添加了一个标记(颜色),同时具有一定的规则。这些规则使红黑树保证了一种平衡,插入、删除、查找的最坏时间复杂度都为 O(logn)。正是因为这个,它的统计性能好于平衡二叉树;在 Java 集合框架中,很多部分(HashMap, TreeMap, TreeSet 等)都有红黑树的应用
红黑树的特性
在原来二叉查找树的基础上,红黑树又增加了以下几个要求
-
每个节点要么是红色,要么是黑色
-
根节点永远是黑色
-
每个叶子节点都是黑色(这里的叶子节点指的是为空的节点)
-
红色节点的两个子节点一定是黑色
-
从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点
同时他也要满足二叉排序树的基本性质:树中的任何节点的值大于它的左子节点,且小于它的右子节点。
这些性质保证了没有一条路径会比其他路径长处两倍,因而,红黑树是相对是接近平衡的二叉树
红黑树的基本操作
1. 插入
对于新插入的节点要先当作按照查找树的规则进行插入,同时也要将他涂成红色,涂成红色是因为:
① 在性质5中,从任意的节点出发到空叶子节点,经过的黑色节点个数是相同的,那么如果你当作黑色系节点插入,肯定会违背这个性质
② 同时由性质5可知,红黑树中黑色节点的个数至少是红色的两倍,所以新增的节点的父亲是黑色的概率较高,如果新增节点的父亲是黑色,而插入的是红色,那么不用再进行调整
但是这样插入可能会违背性质4,也就是说插入节点的父亲是红色的情况
下面是新增节点的几种不同情况
1.1 插入节点为根节点
这个时候只需把插入的红色节点变为黑色节点即可
1.2 插入节点的父亲节点为黑色节点
不需要任何调整
1.3 插入节点的父亲节点为红色,叔叔节点也为红色
因为红色节点的儿子都是黑色,所以要进行调整,策略是
将叔叔节点和父亲节点都变成黑色,爷爷改成红色,再把爷爷当成插入节点,重复上述操作,知道当前节点为根节点,然后根节点变为红色
例子
在下图插入125节点
① 插入125节点并且置为红色
② 将叔叔节点(150)和父亲节点(130)都置为黑色,再把爷爷节点变为红色
③ 然后将140节点当中新的插入节点处理重复以上操作,直到根节点再把根节点涂黑
1.4 新插入节点的父亲为红色,叔叔为黑色
1.4.1 父亲节点为爷爷节点的左孩子,新插入节点为父亲节点的左孩子
将父亲节点和爷爷节点的颜色互换然后针对爷爷进行右旋
例子
下图插入25
把父亲节点和爷爷节点的颜色互换(满足性质4),然后针对爷爷进行右旋(满足性质5)
1.4.2 父亲结点为爷爷结点的右孩子,新插入结点为父亲结点的右孩子
处理方式同上,只不过变为左旋
1.4.3 父亲节点是爷爷节点的左孩子,新插入节点为父亲节点的右节点
先把父亲节点当作当前节点进行左旋,变成1.4.1或1.4.2的情况,在进行调整
例子
下列插入126
① 先针对125进行左旋
② 这就变成1.4.1的情况,将父亲和爷爷交换颜色再以爷爷进行左旋
1.4.4 插入结点是左结点,父亲结点是右结点
以父节点为支进行右旋,再进行操作
private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED; //直接染成红色,少点麻烦
//这里分析的都是父亲节点为红色的情况,不是红色就不用调整了
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) { // 插入节点 x 的父亲节点位于左孩子
Entry<K,V> y = rightOf(parentOf(parentOf(x))); // y 是 x 的叔叔节点
if (colorOf(y) == RED) { //如果 y 也是红色,只要把父亲节点和 y 都变成黑色,爷爷节点变成红的,就 Ok 了
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//特殊情况 待插入节点为右,父亲为左
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else { //和上面对称的操作
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
2. 左旋和右旋详解
进行旋转是为了让一些节点上升一些节点下降,帮助树的平衡
2.1 左旋
此处,以50为支点进行逆时针旋转,然后75成为了顶点,50成为了75的左子节点,65成为了50的右子节点,这个操作就是左旋转。
private void rotateLeft(Entry<K,V> x) {
if (x != null) {
Entry<K,V> y = x.right;
x.right = y.left; // 左旋后,x 的右子树变成了 y 的左子树 β
if (y.left != null)
y.left.parent = x; //β 确认父亲为 x
y.parent = x.parent; //y 取代 x 的第一步:认 x 的父亲为爹
if (x.parent == null) //要是 x 没有父亲,那 y 就是最老的根节点
root = y;
else if (x.parent.left == x) //如果 x 有父亲并且是它父亲的左孩子,x 的父亲现在认 y 为左孩子,不要 x 了
x.parent.left = y;
else //如果 x 是父亲的右孩子,父亲就认 y 为右孩子,抛弃 x
x.parent.right = y;
y.left = x; //y 逆袭成功,以前的爸爸 x 现在成了它的左孩子
y.parent = x;
}
}
2.2 右旋
以75为支点顺时针旋转,然后50成为了顶点,75成为了50的右子节点,65成为了75的左子节点,这就是右旋转操作
private void rotateRight(Entry<K,V> y) {
if (y != null) {
Entry<K,V> x = y.left;
y.left = x.right;
if (x.right != null)
x.right.parent = y;
x.parent = y.parent;
if (y.parent == null)
root = x;
else if (y.parent.right == y)
y.parent.right = x;
else y.parent.left = x;
x.right = y;
y.parent = x;
}
}
3. 删除操作
3.1 删除节点没有儿子
3.1.1 删除节点为红色
这种情况直接删除即可
例子
删除130节点
3.1.2 删除节点为黑色,其兄弟节点没有儿子
在这种情况下,根据红黑树的第五条性质,兄弟节点肯定也是黑色的,这个时候删除节点要把兄弟节点变红,父亲节点变黑
例子
删除150
① 删除150,把兄弟节点126变红,父亲节点140变黑
3.1.3 删除节点为黑,兄弟节点有一个孩子节点,且和兄弟节点在同一边
先交换父亲和兄弟的颜色,再把父亲涂黑,把兄弟节点的子节点涂成黑色,接下来
① 如果兄弟结点和兄弟结点的儿子都在右子树的话:对父亲结点进行左旋
② 如果兄弟结点和兄弟结点的儿子都在左子树的话:对父亲结点进行右旋
例子
来源:CSDN
作者:今天又学java了
链接:https://blog.csdn.net/weixin_43907800/article/details/104073946