1 平衡因子:
左右子树 高度之差
LL型 右旋
LR型 -->LL 右旋
RR -->左旋
RL -->RR 左旋
左旋:逆时针旋转红黑树的两个节点,使得父节点被自己的右孩子取代,而自己成为自己的左孩子。
右旋:顺时针旋转红黑树的两个节点,使得父节点被自己的左孩子取代,而自己成为自己的右孩子
左旋和右旋的代码
/**
* 左旋
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
//这里的p即上图的A节点,r指向右孩子即C,rl指向右孩子的左孩子即D,pp为p的父节点
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
//将p的父节点的孩子节点指向r
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
//将p置为r的左节点
r.left = p;
p.parent = r;
}
return root;
}
/**
* 右旋
*/
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
//这里的p即上图的A节点,l指向左孩子即C,lr指向左孩子的右孩子即E,pp为p的父节点
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
2 红黑树的性质:
1)节点是黑色或者红色
2)根节点是黑色的
3)每个叶结点是黑色的
4)每个红色节点的两个子节点都是黑色的
5)从任一节点到其他每个叶子节点的所有路径都包含相同数目的黑色节点
只要考虑到每个节点都遵循左小右大就很容易判断出是左旋还是右旋
3 红黑树调整
a 插入的节点为红色
因为插入的节点如果为黑色,就可能违背红黑树特性的最后一条,所以每次插入的都是红色节点
b 具体情况
根据被插入节点的父节点的情况,可以将"当节点z被着色为红色节点,并插入二叉树"划分为三种情况来处理。
① 情况说明:被插入的节点是根节点。
处理方法:直接把此节点涂为黑色。
② 情况说明:被插入的节点的父节点是黑色。
处理方法:什么也不需要做。节点被插入后,仍然是红黑树。
③ 情况说明:被插入的节点的父节点是红色。
处理方法:那么,该情况与红黑树的“特性(5)”相冲突。这种情况下,被插入节点是一定存在非空祖父节点的;进一步的讲,被插入节点也一定存在叔叔节点(即使叔叔节点为空,我们也视之为存在,空节点本身就是黑色节点)。理解这点之后,我们依据"叔叔节点的情况",将这种情况进一步划分为3种情况(Case)。
现象说明 | 处理策略 | |
---|---|---|
Case 1 | 当前节点的父节点是红色,且当前节点的祖父节点的另一个子节点(叔叔节点)也是红色 | (01) 将“父节点”设为黑色。(02) 将“叔叔节点”设为黑色。(03) 将“祖父节点”设为“红色”。(04) 将“祖父节点”设为“当前节点”(红色节点);即,之后继续对“当前节点”进行操作。 |
Case 2 | 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的右孩子 | (01) 将“父节点”作为“新的当前节点”。(02) 以“新的当前节点”为支点进行左旋。 |
Case 3 | 当前节点的父节点是红色,叔叔节点是黑色,且当前节点是其父节点的左孩子 | (01) 将“父节点”设为“黑色”。(02) 将“祖父节点”设为“红色”。(03) 以“祖父节点”为支点进行右旋 |
c 删除
第一步:将红黑树当作一颗二叉查找树,将节点删除。
这和"删除常规二叉查找树中删除节点的方法是一样的"。分3种情况:
① 被删除节点没有儿子,即为叶节点。那么,直接将该节点删除就OK了。
② 被删除节点只有一个儿子。那么,直接删除该节点,并用该节点的唯一子节点顶替它的位置。
③ 被删除节点有两个儿子。那么,先找出它的后继节点;然后把“它的后继节点的内容”复制给“该节点的内容”;之后,删除“它的后继节点”。在这里,后继节点相当于替身,在将后继节点的内容复制给"被删除节点"之后,再将后继节点删除。这样就巧妙的将问题转换为"删除后继节点"的情况了,下面就考虑后继节点。 在"被删除节点"有两个非空子节点的情况下,它的后继节点不可能是双子非空。既然"的后继节点"不可能双子都非空,就意味着"该节点的后继节点"要么没有儿子,要么只有一个儿子。若没有儿子,则按"情况① "进行处理;若只有一个儿子,则按"情况② "进行处理。
后继节点就是删除节点比它大的最小子节点
第二步:通过"旋转和重新着色"等一系列来修正该树,使之重新成为一棵红黑树。
因为"第一步"中删除节点之后,可能会违背红黑树的特性。所以需要通过"旋转和重新着色"来修正该树,使之重新成为一棵红黑树。
源码详解
1)TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links 父节点
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion 和next都是用来构建双向链表
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
treeifyBin将普通链表转成为由 TreeNode 型节点组成的链表,并在最后调用 treeify 是将该链表转为红黑树
树化要满足两个条件:
1.链表长度大于等于 TREEIFY_THRESHOLD 8
2.桶数组容量大于等于 MIN_TREEIFY_CAPACITY 64
当桶数组比较小时,键值对节点hash的碰撞率会比较高,进而导致链表长度较长。这个时候应该扩容,而不是树化,而不是立马树化。毕竟高碰撞率是因为桶数组容量较小引起的,这个是主因。同时,扩容时需要拆分红黑树并重新映射,耗费时间。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//符合树化条件
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null