左斜红黑树学习及红黑树在Java的应用

ref:

http://www.cnblogs.com/yangecnu/p/Introduce-Red-Black-Tree.html

https://blog.csdn.net/abcdef314159/article/details/77193888

http://yikun.github.io/2015/04/06/Java-TreeMap%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E7%8E%B0/

左斜红黑树

定义

查找树

查找树是有序的二叉树,同时满足:

  • 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  • 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  • 任意节点的左、右子树也分别为二叉查找树;
  • 没有键值相等的节点

下面以左斜红黑树来讲解红黑树

左斜红黑树

左斜红黑树是一种具有红色和黑色链接的平衡查找树,同时满足:

  • 红色节点向左倾斜
  • 一个节点不可能有两个红色链接
  • 整个书完全黑色平衡,即从根节点到所以叶子结点的路径上,黑色链接的个数都相同。

左斜红黑树查询

红黑树是一种特殊的二叉查找树,他的查找方法也和二叉查找树一样,不需要做太多更改。

但是由于红黑树比一般的二叉查找树具有更好的平衡,所以查找起来更快。

左斜红黑树插入

旋转

旋转又分为左旋右旋。通常左旋操作用于将一个向右倾斜的红色链接旋转为向左链接。对比操作前后,可以看出,该操作实际上是将红线链接的两个节点中的一个较大的节点移动到根节点上。

左旋操作如下图:

左旋的动画效果如下:

Java TreeMap的左旋代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** From CLR */
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;
    }
}

 

颜色反转

当出现一个节点的两个子节点均为红色,如下图:

 

操作方法很简单,就是把E对子节点的连线设置为黑色,自己的颜色设置为红色。

这个代码就不用贴了吧。

插入

Case 1 往一个2-node节点底部插入新的节点

先热身一下,首先我们看对于只有一个节点的红黑树,插入一个新的节点的操作:

这种情况很简单,只需要:

  • 标准的二叉查找树遍历即可。新插入的节点标记为红色
  • 如果新插入的节点在父节点的右子节点,则需要进行左旋操作

Case 2往一个3-node节点底部插入新的节点

先热身一下,假设我们往一个只有两个节点的树中插入元素,如下图,根据待插入元素与已有元素的大小,又可以分为如下三种情况:

  • 如果带插入的节点比现有的两个节点都大,这种情况最简单。我们只需要将新插入的节点连接到右边子树上即可,然后将中间的元素提升至根节点。这样根节点的左右子树都是红色的节点了,我们只需要调研FlipColor方法即可。其他情况经过反转操作后都会和这一样。
  • 如果插入的节点比最小的元素要小,那么将新节点添加到最左侧,这样就有两个连接红色的节点了,这是对中间节点进行右旋操作,使中间结点成为根节点。这是就转换到了第一种情况,这时候只需要再进行一次FlipColor操作即可。
  • 如果插入的节点的值位于两个节点之间,那么将新节点插入到左侧节点的右子节点。因为该节点的右子节点是红色的,所以需要进行左旋操作。操作完之后就变成第二种情况了,再进行一次右旋,然后再调用FlipColor操作即可完成平衡操作。

有了以上基础,我们现在来总结一下往一个3-node节点底部插入新的节点的操作步骤,下面是一个典型的操作过程图:

可以看出,操作步骤如下:

  1. 执行标准的二叉查找树插入操作,新插入的节点元素用红色标识。
  2. 如果需要对4-node节点进行旋转操作
  3. 如果需要,调用FlipColor方法将红色节点提升
  4. 如果需要,左旋操作使红色节点左倾。
  5. 在有些情况下,需要递归调用Case1 Case2,来进行递归操作。如下:

插入套路

大家看明白套路了么:

首先,颜色反转将“问题”向上层抛,也就是解决“问题”的有效手段。

那当我们遇见父子同红色时,就通过旋转将其转换为兄弟同色问题,再通过颜色反转逐步往上解决问题。

另外还有一个问题,为什么父子同红色时,先统一成左斜?我们看下图

这张图除了颜色,还标出了值的大小。刚才说了颜色反转是手段,颜色反转涉及的3个节点的大小顺序是左中右,统一左斜是先把值大小顺序理对。

在红黑树的应用中,不一定是统一左斜的,统一右斜也是可以的,都是先保证值大小顺序。

红黑树在Java中的应用

TreeMap

查询 get

就是二叉查找树的查询算法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final Entry<K,V> getEntry(Object key) {
    // Offload comparator-based version for sake of performance
    if (comparator != null)
        return getEntryUsingComparator(key);
    if (key == null)
        throw new NullPointerException();
    @SuppressWarnings("unchecked")
        Comparable<? super K> k = (Comparable<? super K>) key;
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)
            p = p.left;
        else if (cmp > 0)
            p = p.right;
        else
            return p;
    }
    return null;
}

插入 put

第一步和查询类似,查到key存在替换旧值,key不存在,将新值节点插到底部。

新值插入后的平衡处理和上述左斜红黑类似,不同是存在右斜的情况和处理。平衡的核心代码如下;我在源码基础上做了注释,代码逻辑分支多,但参考我上面的插入套路,不难理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/** From CLR */
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)))) {
            /**
             * A) 左斜处理
             */
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                /**
                 * A.1) 爷爷节点的子节点均红,颜色反转
                 */
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                /**
                 * 循环向上处理
                 */
                x = parentOf(parentOf(x));
            else {
                /**
                 * A.2) 自己和父节点均红
                 */
                if (x == rightOf(parentOf(x))) {
                    /**
                     * 统一成左斜,保证大小顺序
                     */
                    x = parentOf(x);
                    rotateLeft(x);
                }
                /**
                 * 变色加右旋
                 */
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateRight(parentOf(parentOf(x)));
            }
        else {
            /**
             * B) 右斜处理
             */
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                /**
                 * B.1) 爷爷节点的子节点均红,颜色反转
                 */
                setColor(parentOf(x), BLACK);
                setColor(y, BLACK);
                setColor(parentOf(parentOf(x)), RED);
                x = parentOf(parentOf(x));
            else {
                /**
                 * .2) 自己和父节点均红
                 */
                if (x == leftOf(parentOf(x))) {
                    /**
                     * 统一成右斜,保证大小顺序
                     */B
                    x = parentOf(x);
                    rotateRight(x);
                }
                /**
                 * 变色加左旋
                 */
                setColor(parentOf(x), BLACK);
                setColor(parentOf(parentOf(x)), RED);
                rotateLeft(parentOf(parentOf(x)));
            }
        }
    }
    root.color = BLACK;
}

遍历

其实就是中序遍历

在it.next()的调用中会使用nextEntry调用successor方法,代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    else {
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}
  • 空节点没有后继
  • 有右子树则找右子树里最小元素,这个好理解,一直向左查询就行了
  • 没右子树则找祖先里最小的;如果自己是父亲的左子树,则符合中序遍历的原则。如果是右子树,则自己是最右节点的"中"(即中是父亲的左节点),"中"的父亲就是后继节点

Java8 HashMap

注意本图是示意图,默认转换红黑树的阀值TREEIFY_THRESHOLD是8个。

代码20行是转换为红黑树,14行是红黑树的节点插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1// -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

对比其他数据结构

跳跃表

和红黑树类似,有序+快速访问。在并发情况下,红黑平衡调整范围可能比跳跃表大。举个例子,Java里的基于跳跃表的ConcurrentSkipListMap修改集合元素操作可以基于无锁的CAS操作。

B树

posted @ 2018-04-03 11:46  litefy  阅读(561)  评论(0编辑  收藏  举报