前言
HashMap是一个有序的<K,V>结构,内部是由红黑树实现的。红黑树是一个平衡的二叉查找树,最坏的查找是件复杂度是O(logn)的,也就是说十亿条数据最差只需要30次比较,就能够找出来。
红黑树简单介绍
阅读源码之前,需要对红黑树有个简单的了解。 红黑树的五个特性:
- 节点是红色或黑色
- 根节点是黑色
- 每个红色节点的两个子节点都是黑色(两个红色结点不能相连)
- 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
- 叶子结点(NIL)是黑色
红黑树保持平衡的方法有三种:变色、左旋、右旋。具体的平衡逻辑就不在这里细说了,有一篇非常好的文章建议阅读一下30张图带你彻底理解红黑树
TreeMap的基本方法
对结点p左旋
- p的右结点变为它的父结点
- p的右结点的左结点,成为p的右结点
private void rotateLeft(Entry<K,V> p) {
if (p != null) {
//临时变量r=该结点的右结点
Entry<K,V> r = p.right;
//它右结点的左结点,变为该结点的右结点
p.right = r.left;
if (r.left != null)
r.left.parent = p;
//以下操作使临时变量r变为了它的父结点
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;
}
}
对结点p右旋
- p的左结点变为p的父结点
- p的左结点的右结点,成为该结点的左结点
private void rotateRight(Entry<K,V> p) {
if (p != null) {
//临时变量l=它的左结点
Entry<K,V> l = p.left;
//它的左结点,变为它的左结点的右结点
p.left = l.right;
if (l.right != null) l.right.parent = p;
//以下操作使临时变量l变为了它的父结点
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;
}
}
新增结点x后的平衡
首先,可以显而易见的看到,我们一开始默认插入的结点为红色,而因为红黑树的特性,在最后一行代码可以看到,根结点一定会变为黑色。 然后根据if-else操作,一共分为了7个场景 具体的就不列出来了,总结一下
- 当父结点为黑结点时,并不会影响到平衡,所以无需做任何改变
- 当父结点为红色时,根据规范,不可以有两个红色的结点相连,所以先要试试变色,叔叔结点与父结点颜色相同的话,只需变色即可
- 当插入一个元素时,它的叔叔结点和父结点的颜色肯定相同,不然的话它之前就不符合红黑树的特性了,即根结点到任一叶子结点经过黑结点数量相同。那么只有一种可能性,叔叔结点为null。此时需要旋转来达到平衡。
- 最后根结点一定要变成黑色!哪怕变成了一颗黑黑红树!
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)))) {
//y是x的叔叔结点,也就是父结点的兄弟结点
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//当叔叔结点为红色
if (colorOf(y) == RED) {
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
//当叔叔结点为黑色
} else {
//当x为右结点时,需要多一步左旋操作
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 {
//当x为左结点时,需要多一步右旋操作
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;
}
删除结点p
跟增加相比,删除结点比较复杂
- p没有子结点,直接删除即可
- p只有一个子结点,那么用子结点替换掉删除的结点
- p包含两个子结点,那么用后继结点(大于删除结点的最小结点)来替换 依据上述步骤找到替换结点之后,就要开始根据这个替换结点来做平衡操作了。
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
//当p有俩结点的情况下
if (p.left != null && p.right != null) {
//找到后继结点,将删除结点的k-v换成后继结点,然后把后继结点之前的位置,当作待删除结点。由于后继结点一定没有左结点,可能存在右结点,所以可以在下面视作前两种场景处理。
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
}
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
//p只有一个结点的情况下
if (replacement != null) {
// Link replacement to parent
//p的左结点替换掉p
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
删掉p结点
p.left = p.right = p.parent = null;
//如果p是黑色,需要平衡操作
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { //如果我们只有这一个结点,直接删了返回就行
root = null;
} else {
//没有子结点,直接删除,如果是黑色,需要进行平衡操作
if (p.color == BLACK)
fixAfterDeletion(p);
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}
删除结点后的平衡操作
为了方便理解,下文中用D标记被删除结点、用R标记替换结点。 基于上段代码的理解,我们目前有个前提“被删除的结点是黑色”。 删除操作平衡场景分支较多,大致可以分为三种:靠自己、靠兄弟、靠父母。
- 靠自己 ---自己变黑
- 若D无子结点且为红色,那么无需平衡。
- 若D存在1个子结点,所以子结点即为R,R一定是红色的,无需平衡。
- 若D存在2个子结点,那么找到它的后继结点,标记为R,此时R最多只有一个子结点,若R为红色无需平衡
- 靠兄弟 ---让兄弟变红
- 若D无子结点且为黑色,且兄弟为黑色,那么兄弟只可能存在红色子结点,或者不存在结点。以下兄弟是右子结点为例,左子结点相反即可。
- 若兄弟无子结点,直接让兄弟变红,父亲变黑,即可达到平衡
- 若兄弟只存在左子结点,那么说明对父亲左旋后,会变瘸,故需要先对兄弟右旋。
- 若兄弟存在右子结点,那么直接对父亲左旋即可平衡。
- 若D存在两个结点、且R无子结点且为黑色,则将R当作D,操作跟上述一样。
- 若D存在两个结点、且R有右结点且为黑色,那么R的右结点一定是红色,此时直接用R的子结点替换掉R即可平衡。
- 靠父母 ---让兄弟变红的前提下,让父亲变黑。
- 如果D无子结点且为黑色,兄弟也没有结点且为黑色。那么则需将兄弟结点变为红色,且将父结点结点变为黑色
- 如果父结点本来就是黑色,那就要让父结点去找它的兄弟了,以此类推
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));
//如果兄弟结点是红色,兄弟变黑,父亲变红,左旋,此时兄弟的左结点变为了x新的兄弟。
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
//如果兄弟没有子结点,兄弟结点变红,让父结点去找它的兄弟
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
//如果兄弟有存在有右结点,说明左旋会瘸腿,要先对兄弟右旋
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
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));
//兄弟是红色,兄弟变黑,父亲变红,右旋,此时兄弟的右结点是x新的兄弟
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}
//兄弟无子结点,且为黑色,则变红,让父结点继续找它的兄弟
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
//如果兄弟不存在左结点,则右旋会瘸,要先对兄弟左旋
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
//对父结点右旋
rotateRight(parentOf(x));
x = root;
}
}
}
//根结点一定要变为黑色
setColor(x, BLACK);
}
后记
红黑树这部分的知识还算是比较复杂的,总结一下
- 插入或者删除一个红结点维持不变
- 删除一个黑结点相当于把它从黑色变为红色,然后从下往上一层层的把红色赶出去,是一个自底向上的操作
本文只作为学习过程中的随笔,如果有不正确的地方欢迎指出。