Java集合(4)一 红黑树、TreeMap与TreeSet(下)

时间:2021-03-17 14:42:45

目录

Java集合(1)一 集合框架

Java集合(2)一 ArrayList 与 LinkList

Java集合(3)一 红黑树、TreeMap与TreeSet(上)

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

Java集合(5)一 HashMap与HashSet

引言

Java集合(3)一 红黑树、TreeMap与TreeSet(上)中从二叉树的遍历、添加和删除引申到了红黑树的遍历、添加和删除。对二叉树结构有了一定的了解,在这篇文章中将会对红黑树进行详细的说明。

红黑树

二叉树在理想情况下时间复杂度是O(logn),最坏情况下当插入的数据由小到大或者由大到小排列的时候,二叉树就变成了一个链表,而我们知道链表检索的时间复杂度是O(n),效率非常差,所以出现了AVL树和红黑树来改变这种状况。同时由于AVL树的极端平衡特性,导致添加和删除数据后需要过多的旋转操作来保证AVL树平衡的特征,所以TreeMap中会使用红黑树来存储数据。

最好情况下的二叉树:

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

最差情况下的二叉树:

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

树旋转

当AVL树在添加或者删除节点时出现不平衡后通过什么操作来保证树的平衡性呢?这种操作就叫做书旋转。红黑树也是如此,不过红黑树更复杂,这点我们后面再说,先来看看AVL树的旋转操作。

树旋转操作是由于二叉树在添加节点时为了避免出现平衡失效的情况而做的一种操作,操作的基本原则是操作后不影响二叉树中序遍历的结果。

这里我们用AVL树来说明这个问题。AVL树是一种高度平衡的二叉树,他的任何两个节点的子树的高度最大差别为1,这样他的查找、插入和删除的时间复杂度都是O(logn),当出现不平衡情况的时候,就需要执行树旋转。

旋转操作

树的旋转操作分为两种,左旋转和右旋转,这两种旋转是相对的。通过右旋或者左旋操作我们可以使一棵树继续保持平衡状态,并不会改变中序遍历的结果,但同时也要付出相应的代价。

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

//右旋
private void rotateRight(Entry<K,V> p) {
if (p != null) {
Entry<K,V> l = p.left;
p.left = l.right;
if (l.right != null) l.right.parent = p;
l.parent = p.parent;
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l;
else p.parent.left = l;
l.right = p;
p.parent = l;
}
}
//左旋
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
Entry<K,V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}

AVL树旋转的几种情况

当树的任何两个节点的子树的高度差大于1的时候,就需要进行旋转以保证任何两个节点的子树的高度最大差别为1。哪几种情况下需要进行树旋转操作?

1.左左情况,左节点比右节点多两个节点,并且多出的节点都在左子树;

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

2.右右情况,右节点比左节点多两个节点,并且多出的节点都在右子树;

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

3.左右情况,左节点或者右节点多出两个节点,多出的第一个节点在左子树,第二个节点在右子树;

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

4.右左情况,左节点或者右节点多出两个节点,多出的第一个节点在右子树,第二个节点在左子树;

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

红黑树的特性

明白了AVL树的旋转操作,再来看红黑树就简单多了,红黑树就是一颗满足一定条件的,相对平衡的二叉树,是二叉树和AVL树的一种折中。

红黑树的添加删除操作同二叉树一样,但是当添加删除等操作后使红黑树失去了他的特性后,就需要进行旋转操作来恢复红黑树的特性。

红黑树需要满足以下几点性质:

1.每个节点要么是红色,要么是黑色。

2.根节点永远是黑色的。

3.所有的叶节点都是空节点(即 null),并且是黑色的。

4.每个红色节点的两个子节点都是黑色。(从每个叶子到根的路径上不会有两个连续的红色节点)

5.从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。

性质1和性质2很好理解。性质3在Java里面指的是空节点,一般不用考虑。性质4保证了从根到叶子节点的最长路径最多只能是最短路径的两倍,根据性质5建立一颗黑色节点为3的红黑树,最短路径为黑-黑-黑,最长路径为黑-红-黑-红-黑-红(因为每个红色节点的两个子节点都是黑色,红色则不可连续)。

下图是一颗标准的红黑树:

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

红黑树添加后修复

在上一篇文章中的添加操作后调用了fixAfterInsertion(e)方法用来修复被破坏的红黑树性质。

一般默认每个添加的节点都为红色,因为添加的节点如果为黑色,那就一定会破坏性质5,而且很难修复。但如果添加的是红色节点,有可能就不会破坏任何性质,也可能会破坏性质4导致连续的红色节点,可以通过变色和旋转来修复。

在添加红色节点时可能会遇到以下几种情况:

1.新节点为根节点,父节点为空,破坏性质2,修复红色为黑色即可。

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

2.新节点的父节点为黑色,添加的新节点为红色,不破坏任何性质。

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

3.新节点的父节点为红色,同时父节点的兄弟节点也为红色(根据性质4,父节点的父节点为黑色),添加的新节点也为红色,破坏了性质4,修复父节点和父节点的兄弟节点为黑色,同时父节点的父节点为红色,保证性质5不被破坏。

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

4.新节点的父节点为红色,同时父节点的兄弟节点为黑色或为空(空也为黑色)。如果新节点为父节点的左节点,但新节点的父节点为祖父节点的右节点;或者新节点为父节点的右节点,但新节点的父节点为祖父节点的左节点,就需要先右旋或者左旋,然后转换成情况5,再进行一次着色和旋转。

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

5.新节点的父节点为红色,同时父节点的兄弟节点为黑色或为空(空也为黑色)。如果新节点为父节点的左节点,同时新节点的父节点也为祖父节点的左节点;或者新节点为父节点的右节点,同时新节点的父节点也为祖父节点的右节点。设置新节点的父节点为黑色,设置新节点的祖父节点为红色,然后左旋或者右旋即可。

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

private void fixAfterInsertion(Entry<K,V> x) {
x.color = RED;
//情况2 x.parent.color == BLACK
while (x != null && x != root && x.parent.color == RED) {
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//情况3 父节点的兄弟节点也为红色 不需要旋转
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//情况4 父节点的兄弟节点为黑色 父节点为祖父节点的左节点 x为父节点的右节点 先左旋变为情况5
if (x == rightOf(parentOf(x))) {
x = parentOf(x);
rotateLeft(x);
}
//情况5 父节点的兄弟节点为黑色 父节点为祖父节点的左节点 x也为父节点的左节点 改变父节点为黑色 祖父节点为红色 然后祖父节点右旋
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
//情况3
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//情况4 父节点的兄弟节点为黑色 父节点为祖父节点的右节点 x为父节点的左节点 先右旋变为情况5
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
//情况5 父节点的兄弟节点为黑色 父节点为祖父节点的右节点 x也为父节点的右节点 改变父节点为黑色 祖父节点为红色 然后祖父节点左旋
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
//情况1
root.color = BLACK;
}

红黑树删除后修复

红黑树在删除后调用了fixAfterDeletion(p)用来修复被破坏的红黑树性质。

由于在删除时我们采用后继节点替换的方法,替换之后只需要删除替换的节点即可。这样删除节点的问题就可以转换为删除至多只有1个孩子节点的问题(因为后继节点至多只有右孩子,或者没有孩子)。

删除时有以下几种情况:

1.删除的节点为红色,根据红色节点不可连续,则他的父节点和子节点都为黑色,直接用他的黑色子节点替换即可,删除了红色节点不会破坏性质5。

2.删除的节点为黑色,但是儿子为红色,直接用红色节点替换,替换后变红色节点为黑色即可。

3.删除的节点为黑色,同时儿子也为黑色,这种情况比较复杂,又可以分为几种情况:

将儿子命名为S,儿子替换后的父亲命名为F,儿子替换后的兄弟命名为B,兄弟的左节点命名为BL,兄弟的右节点命名为BR,情况3又可以分为以下几种情况:

4.F为任意色,B为红色,将F左旋转,并交换F和B的颜色,则通过各自路径的黑色节点数目不变,但S现在有了一个红色的父节点F,一个黑色的兄弟节点B,则情况可以变成5、6或者7。

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

5.F为任意色,B为黑色,BL和BR也为黑色,只需要将B的颜色设置为红色,则通过B的路径少了一个黑色节点和通过S的黑色节点相等了,但通过F的路径少了一个黑色节点,可以重新从第一种情况进行迭代。

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

6.F为任意色,B为黑色,BL为红色,BR为黑色,将B右旋,这样就变成了情况7。

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

7.F为任意色,B为黑色,BL为黑色,BR为红色,将F左旋,同时交换F和B和颜色即可。

Java集合(4)一 红黑树、TreeMap与TreeSet(下)

private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
//情况4 F为任意色,B为红色 情况可以变成5、6或者7
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
//情况5 F为任意色,B为黑色,BL和BR也为黑色
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
//情况6 F为任意色,B为黑色,BL为红色,BR为黑色
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
//情况7 F为任意色,B为黑色,BL为黑色,BR为红色
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
Entry<K,V> sib = leftOf(parentOf(x));
//情况4 F为任意色,B为红色 情况可以变成5、6或者7
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}
//情况5 F为任意色,B为黑色,BL和BR也为黑色
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
//情况6 F为任意色,B为黑色,BL为红色,BR为黑色 旋转着色后变为情况7
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
//情况7 F为任意色,B为黑色,BL为黑色,BR为红色
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
} setColor(x, BLACK);
}

总结

理解了红黑树遍历,删除添加等操作的分析,再理解TreeMap<K,V>实现逻辑就会很容易。TreeMap<K,V>所有的操作都是在红黑树的基础上执行的,红黑树的每一个节点对应为TreeMap<K,V>的一个Entry<K,V>。

TreeSet<E>由于在实现上完全使用了TreeMap<K,V>的key来实现,所以TreeSet<E>的所有操作一样是建立在红黑树的基础上。