Java源码阅读——TreeMap和红黑树

时间:2020-12-02 17:17:35

Java源码阅读——TreeMap和红黑树

红黑树

什么是红黑树

红黑树是基于二叉搜索树的,为了能以较快的时间O(logN)来搜索一棵树,需要保证树总是平衡的(或者至少大部分是平衡的),这就是说对树中的每个节点在它左边的后代数目和在它右边的后代数目应该大致相等。红黑树就是这样的一棵平衡树,对一个要插入的数据项,插入例程要检查会不会破坏树的特征,如果破坏了,程序就会进行纠正,根据需要改变树的结构,从而保持树的自平衡

红黑树特征

红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

l  性质 1 节点是红色或黑色。

l  性质 2 根节点是黑色。

l  性质 3 每个叶节点(NIL节点,空节点)是黑色的。

l  性质 4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)

l  性质 5 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。

以上来自百度百科的一些概念,红黑树无非就是一种自平衡(根据检查以上性质进行调整树的结构)的二叉搜索树。

修正

为什么要修正?当然是插入节点后破坏了红黑树的性质进行结构上的一种调整。插入的节点都是红色的。插入黑色节点总会改变黑色高度(违背性质5),但是插入红色节点只有一半的机会会违背性质4。另外违背性质4比违背性质5要更容易修正。当插入一个新的节点时,可能会破坏这种平衡性。红黑树可以通过变色、左旋和右旋来调整结构。

 

左旋和右旋

下面两幅图比较形象的描述了左旋和右旋的情形。

Java源码阅读——TreeMap和红黑树

Java源码阅读——TreeMap和红黑树

TreeMap

定义

public class TreeMap<K,V>extends AbstractMap<K,V>
   
implements NavigableMap<K,V>, Cloneable, java.io.Serializable

继承了AbstractMap抽象类,实现了NavigableMap等接口,其中NavigableMap继承了SortedMap接口。也就是说该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator 进行排序,具体取决于使用的构造方法。 此实现为 containsKey、get、put 和 remove 操作提供受保证的 log(n) 时间开销。

TreeMap中的左旋和右旋

结合上面的图比较好理解,就是改变节点之间指向关系。

/** 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;
   
}
}

/** From CLR */
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;
   
}
}

 

TreeMap中的结构调整

插入节点后进行调整。

设置插入节点的颜色为RED红色。下面一系列的if判断,parentOf(x)表示x的父节点,leftOf(x)表示x左节点,rightOf(x)表示x的右节点,根据上面提到的性质和变色左旋右旋的操作进行调整。理解了黑红树的性质后,下面代码还是比较好理解的。

/** 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)))) {            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 {                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;}

删除节点后进行调整

/** From CLR */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));            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));            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);}

常用方法

V get(Object key)

返回指定键所映射的值,如果对于该键而言,此映射不包含任何映射关系,则返回 null。

public V get(Object key) {    Entry<K,V> p = getEntry(key);    return (p==null ? null : p.value);}

在getEntry(key)的代码中有这么一段:

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;}

即查找的过程。

V put(K key, V value)

将指定值与此映射中的指定键进行关联。

这里先注意下

Comparator<? super K> cpr =comparator;

if (cpr != null){.代码1.}eles{.代码2.}

代码1和代码2中都有左查右查的语句,comparator即比较器,用于在此树形图中维护顺序,如果使用键的自然顺序,则为null,也就是key键比较规则。

这段代码比较好理解,首先在红黑树上找到合适的位置,然后创建新的entry并插入(当然,新插入的节点一定是树的叶子)。难点是调整函数fixAfterInsertion(),前面已经说过,调整往往需要1.改变某些节点的颜色,2.对某些节点进行旋转。

public V put(K key, V value) {    Entry<K,V> t = root;    if (t == null) {        compare(key, key); // type (and possibly null) check        root = new Entry<>(key, value, null);        size = 1;        modCount++;        return null;    }    int cmp;    Entry<K,V> parent;    // split comparator and comparable paths    Comparator<? super K> cpr = comparator;    if (cpr != null) {        do {            parent = t;            cmp = cpr.compare(key, t.key);            if (cmp < 0)                t = t.left;            else if (cmp > 0)                t = t.right;            else                return t.setValue(value);        } while (t != null);    }    else {        if (key == null)            throw new NullPointerException();        @SuppressWarnings("unchecked")            Comparable<? super K> k = (Comparable<? super K>) key;        do {            parent = t;            cmp = k.compareTo(t.key);            if (cmp < 0)                t = t.left;            else if (cmp > 0)                t = t.right;            else                return t.setValue(value);        } while (t != null);    }    Entry<K,V> e = new Entry<>(key, value, parent);    if (cmp < 0)        parent.left = e;    else        parent.right = e;    fixAfterInsertion(e);    size++;    modCount++;    return null;}

V remove(Object key)

如果此 TreeMap 中存在该键的映射关系,则将其删除。

remove(Object key)的作用是删除key值对应的entry,该方法首先通过getEntry(Object key)方法找到key值对应的entry,然后调用deleteEntry(Entry<K,V>entry)删除对应的entry。由于删除操作会改变红黑树的结构,有可能破坏红黑树的约束,因此有可能要通过fixAfterDeletion(p)进行调整。

public V remove(Object key) {    Entry<K,V> p = getEntry(key);    if (p == null)        return null;    V oldValue = p.value;    deleteEntry(p);    return oldValue;}

一棵普通二叉查找树的删除过程,可以简单分为两种情况:

1、删除点p的左右子树都为空,或者只有一棵子树非空。

2、删除点p的左右子树都非空。

对于上述情况1,处理起来比较简单,直接将p删除(左右子树都为空时),或者用非空子树替代p(只有一棵子树非空时);对于情况2,可以用p的后继s(树中大于x的最小的那个元素)代替p,然后使用情况1删除s(此时s一定满足情况1,可以画画看)。

private void deleteEntry(Entry<K,V> p) {    modCount++;    size--;    // If strictly internal, copy successor's element to p and then make p    // point to successor.    if (p.left != null && p.right != null) {        Entry<K,V> s = successor(p);        p.key = s.key;        p.value = s.value;        p = s;    } // p has 2 children    // Start fixup at replacement node, if it exists.    Entry<K,V> replacement = (p.left != null ? p.left : p.right);    if (replacement != null) {        // Link replacement to parent        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;        // Null out links so they are OK to use by fixAfterDeletion.        p.left = p.right = p.parent = null;        // Fix replacement        if (p.color == BLACK)            fixAfterDeletion(replacement);    } else if (p.parent == null) { // return if we are the only node.        root = null;    } else { //  No children. Use self as phantom replacement and unlink.        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;        }    }}

getCeilingEntry

获得该节点的下一节点,即min(>=p)。找到后序节点。

final Entry<K,V> getCeilingEntry(K key) {    Entry<K,V> p = root;    while (p != null) {        int cmp = compare(key, p.key);        if (cmp < 0) {            if (p.left != null)                p = p.left;            else                return p;        } else if (cmp > 0) {            if (p.right != null) {                p = p.right;            } else {                Entry<K,V> parent = p.parent;                Entry<K,V> ch = p;                while (parent != null && ch == parent.right) {                    ch = parent;                    parent = parent.parent;                }                return parent;            }        } else            return p;    }    return null;}

涉及到该方法的两个public方法如下:

Map.Entry<K,V> ceilingEntry(K key)

返回一个键-值映射关系,它与大于等于给定键的最小键关联;如果不存在这样的键,则返回null。

K ceilingKey(K key)

返回大于等于给定键的最小键;如果不存在这样的键,则返回 null。

getFloorEntry

获得该节点的上一节点,即max(<=p)。找到前序节点。

final Entry<K,V> getFloorEntry(K key) {    Entry<K,V> p = root;    while (p != null) {        int cmp = compare(key, p.key);        if (cmp > 0) {            if (p.right != null)                p = p.right;            else                return p;        } else if (cmp < 0) {            if (p.left != null) {                p = p.left;            } else {                Entry<K,V> parent = p.parent;                Entry<K,V> ch = p;                while (parent != null && ch == parent.left) {                    ch = parent;                    parent = parent.parent;                }                return parent;            }        } else            return p;    }    return null;}

涉及到该方法的两个public方法如下:

Map.Entry<K,V> floorEntry(K key)

返回一个键-值映射关系,它与小于等于给定键的最大键关联;如果不存在这样的键,则返回null。

K floorKey(K key)

返回小于等于给定键的最大键;如果不存在这样的键,则返回 null。

以上的方法特别能体现TreeMap基于红黑树的一些性质的实现,其他的方法,结合HashMap查看相关API。