史上最清晰的红黑树讲解

本文以Java TreeMap为例,从源代码层面,结合详细的图解,剥茧抽丝地讲解红黑树(Red-Black tree)的插入,删除以及由此产生的调整过程。java

整体介绍

Java TreeMap实现了SortedMap接口,也就是说会按照key的大小顺序对Map中的元素进行排序,key大小的评判能够经过其自己的天然顺序(natural ordering),也能够经过构造时传入的比较器(Comparator)。程序员

TreeMap底层经过红黑树(Red-Black tree)实现,也就意味着containsKey()get()put()remove()都有着log(n)的时间复杂度。其具体算法实现参照了《算法导论》。算法

TreeMap_base.png

出于性能缘由,TreeMap是非同步的(not synchronized),若是须要在多线程环境使用,须要程序员手动同步;或者经过以下方式将TreeMap包装成(wrapped)同步的:多线程

SortedMap m = Collections.synchronizedSortedMap(new TreeMap(...));

红黑树是一种近似平衡的二叉查找树,它可以确保任何一个节点的左右子树的高度差不会超过两者中较低那个的一陪。具体来讲,红黑树是知足以下条件的二叉查找树(binary search tree):app

  1. 每一个节点要么是红色,要么是黑色。
  2. 根节点必须是黑色
  3. 红色节点不能连续(也便是,红色节点的孩子和父亲都不能是红色)。
  4. 对于每一个节点,从该点至null(树尾端)的任何路径,都含有相同个数的黑色节点。

在树的结构发生改变时(插入或者删除操做),每每会破坏上述条件3或条件4,须要经过调整使得查找树从新知足红黑树的条件。函数

预备知识

前文说到当查找树的结构发生改变时,红黑树的条件可能被破坏,须要经过调整使得查找树从新知足红黑树的条件。调整能够分为两类:一类是颜色调整,即改变某个节点的颜色;另外一类是结构调整,集改变检索树的结构关系。结构调整过程包含两个基本操做:左旋(Rotate Left),右旋(RotateRight)性能

左旋

左旋的过程是将x的右子树绕x逆时针旋转,使得x的右子树成为x的父亲,同时修改相关节点的引用。旋转以后,二叉查找树的属性仍然知足。this

TreeMap_rotateLeft.png

TreeMap中左旋代码以下:spa

//Rotate Left
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;
    }
}


右旋

右旋的过程是将x的左子树绕x顺时针旋转,使得x的左子树成为x的父亲,同时修改相关节点的引用。旋转以后,二叉查找树的属性仍然知足。线程

TreeMap_rotateRight.png

TreeMap中右旋代码以下:

//Rotate Right
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;
    }
}


方法剖析

get()

get(Object key)方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)获得相应的entry,而后返回entry.value。所以getEntry()是算法的核心。算法思想是根据key的天然顺序(或者比较器顺序)对二叉查找树进行查找,直到找到知足k.compareTo(p.key) == 0entry

TreeMap_getEntry.png

具体代码以下:

//getEntry()方法
final Entry<K,V> getEntry(Object key) {
    ......
    if (key == null)//不容许key值为null
        throw new NullPointerException();
    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()

put(K key, V value)方法是将指定的keyvalue对添加到map里。该方法首先会对map作一次查找,看是否包含该元组,若是已经包含则直接返回,查找过程相似于getEntry()方法;若是没有找到则会在红黑树中插入新的entry,若是插入以后破坏了红黑树的约束,还须要进行调整(旋转,改变某些节点的颜色)。

public V put(K key, V value) {
    ......
    int cmp;
    Entry<K,V> parent;
    if (key == null)
        throw new NullPointerException();
    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);//建立并插入新的entry
    if (cmp < 0) parent.left = e;
    else parent.right = e;
    fixAfterInsertion(e);//调整
    size++;
    return null;
}

上述代码的插入部分并不难理解:首先在红黑树上找到合适的位置,而后建立新的entry并插入(固然,新插入的节点必定是树的叶子)。难点是调整函数fixAfterInsertion(),前面已经说过,调整每每须要1.改变某些节点的颜色,2.对某些节点进行旋转。

TreeMap_put.png

调整函数fixAfterInsertion()的具体代码以下,其中用到了上文中提到的rotateLeft()rotateRight()函数。经过代码咱们可以看到,状况2实际上是落在状况3内的。状况4~状况6跟前三种状况是对称的,所以图解中并无画出后三种状况,读者能够参考代码自行理解。

//红黑树调整函数fixAfterInsertion()
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) {//若是y为null,则视为BLACK
                setColor(parentOf(x), BLACK);              // 状况1
                setColor(y, BLACK);                        // 状况1
                setColor(parentOf(parentOf(x)), RED);      // 状况1
                x = parentOf(parentOf(x));                 // 状况1
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);                       // 状况2
                    rotateLeft(x);                         // 状况2
                }
                setColor(parentOf(x), BLACK);              // 状况3
                setColor(parentOf(parentOf(x)), RED);      // 状况3
                rotateRight(parentOf(parentOf(x)));        // 状况3
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);              // 状况4
                setColor(y, BLACK);                        // 状况4
                setColor(parentOf(parentOf(x)), RED);      // 状况4
                x = parentOf(parentOf(x));                 // 状况4
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);                       // 状况5
                    rotateRight(x);                        // 状况5
                }
                setColor(parentOf(x), BLACK);              // 状况6
                setColor(parentOf(parentOf(x)), RED);      // 状况6
                rotateLeft(parentOf(parentOf(x)));         // 状况6
            }
        }
    }
    root.color = BLACK;
}


remove()

remove(Object key)的做用是删除key值对应的entry,该方法首先经过上文中提到的getEntry(Object key)方法找到key值对应的entry,而后调用deleteEntry(Entry<K,V> entry)删除对应的entry。因为删除操做会改变红黑树的结构,有可能破坏红黑树的约束,所以有可能要进行调整。

 

上面对Java TreeMap的插入以及插入以后的调整过程给出了详述。下面接着以Java TreeMap为例,从源码层面讲解红黑树的删除,以及删除以后的调整过程。

寻找节点后继

对于一棵二叉查找树,给定节点t,其后继(树种比大于t的最小的那个元素)能够经过以下方式找到:

  1. t的右子树不空,则t的后继是其右子树中最小的那个元素。
  2. t的右孩子为空,则t的后继是其第一个向左走的祖先。

后继节点在红黑树的删除操做中将会用到。

TreeMap_successor.png

TreeMap中寻找节点后继的代码以下:

// 寻找节点后继函数successor()
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
    if (t == null)
        return null;
    else if (t.right != null) {// 1. t的右子树不空,则t的后继是其右子树中最小的那个元素
        Entry<K,V> p = t.right;
        while (p.left != null)
            p = p.left;
        return p;
    } else {// 2. t的右孩子为空,则t的后继是其第一个向左走的祖先
        Entry<K,V> p = t.parent;
        Entry<K,V> ch = t;
        while (p != null && ch == p.right) {
            ch = p;
            p = p.parent;
        }
        return p;
    }
}

remove()

remove(Object key)的做用是删除key值对应的entry,该方法首先经过上文中提到的getEntry(Object key)方法找到key值对应的entry,而后调用deleteEntry(Entry<K,V> entry)删除对应的entry。因为删除操做会改变红黑树的结构,有可能破坏红黑树的约束条件,所以有可能要进行调整。

getEntry()函数前面已经讲解过,这里重点放deleteEntry()上,该函数删除指定的entry并在红黑树的约束被破坏时进行调用fixAfterDeletion(Entry<K,V> x)进行调整。

因为红黑树是一棵加强版的二叉查找树,红黑树的删除操做跟普通二叉查找树的删除操做也就很是类似,惟一的区别是红黑树在节点删除以后可能须要进行调整。如今考虑一棵普通二叉查找树的删除过程,能够简单分为两种状况:

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

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

基于以上逻辑,红黑树的节点删除函数deleteEntry()代码以下:

// 红黑树entry删除函数deleteEntry()
private void deleteEntry(Entry<K,V> p) {
    modCount++;
    size--;
    if (p.left != null && p.right != null) {// 2. 删除点p的左右子树都非空。
        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);
    if (replacement != null) {// 1. 删除点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.left = p.right = p.parent = null;
        if (p.color == BLACK)
            fixAfterDeletion(replacement);// 调整
    } else if (p.parent == null) {
        root = null;
    } else { // 1. 删除点p的左右子树都为空
        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;
        }
    }
}

上述代码中占据大量代码行的,是用来修改父子节点间引用关系的代码,其逻辑并不难理解。下面着重讲解删除后调整函数fixAfterDeletion()。首先请思考一下,删除了哪些点才会致使调整?只有删除点是BLACK的时候,才会触发调整函数,由于删除RED节点不会破坏红黑树的任何约束,而删除BLACK节点会破坏规则4。

跟上文中讲过的fixAfterInsertion()函数同样,这里也要分红若干种状况。记住,不管有多少状况,具体的调整操做只有两种:1.改变某些节点的颜色,2.对某些节点进行旋转。

TreeMap_fixAfterDeletion.png

上述图解的整体思想是:将状况1首先转换成状况2,或者转换成状况3和状况4。固然,该图解并不意味着调整过程必定是从状况1开始。经过后续代码咱们还会发现几个有趣的规则:a).若是是由状况1以后紧接着进入的状况2,那么状况2以后必定会退出循环(由于x为红色);b).一旦进入状况3和状况4,必定会退出循环(由于x为root)。

删除后调整函数fixAfterDeletion()的具体代码以下,其中用到了上文中提到的rotateLeft()rotateRight()函数。经过代码咱们可以看到,状况3实际上是落在状况4内的。状况5~状况8跟前四种状况是对称的,所以图解中并无画出后四种状况,读者能够参考代码自行理解。

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);                   // 状况1
                setColor(parentOf(x), RED);             // 状况1
                rotateLeft(parentOf(x));                // 状况1
                sib = rightOf(parentOf(x));             // 状况1
            }
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                setColor(sib, RED);                     // 状况2
                x = parentOf(x);                        // 状况2
            } else {
                if (colorOf(rightOf(sib)) == BLACK) {
                    setColor(leftOf(sib), BLACK);       // 状况3
                    setColor(sib, RED);                 // 状况3
                    rotateRight(sib);                   // 状况3
                    sib = rightOf(parentOf(x));         // 状况3
                }
                setColor(sib, colorOf(parentOf(x)));    // 状况4
                setColor(parentOf(x), BLACK);           // 状况4
                setColor(rightOf(sib), BLACK);          // 状况4
                rotateLeft(parentOf(x));                // 状况4
                x = root;                               // 状况4
            }
        } else { // 跟前四种状况对称
            Entry<K,V> sib = leftOf(parentOf(x));
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);                   // 状况5
                setColor(parentOf(x), RED);             // 状况5
                rotateRight(parentOf(x));               // 状况5
                sib = leftOf(parentOf(x));              // 状况5
            }
            if (colorOf(rightOf(sib)) == BLACK &&
                colorOf(leftOf(sib)) == BLACK) {
                setColor(sib, RED);                     // 状况6
                x = parentOf(x);                        // 状况6
            } else {
                if (colorOf(leftOf(sib)) == BLACK) {
                    setColor(rightOf(sib), BLACK);      // 状况7
                    setColor(sib, RED);                 // 状况7
                    rotateLeft(sib);                    // 状况7
                    sib = leftOf(parentOf(x));          // 状况7
                }
                setColor(sib, colorOf(parentOf(x)));    // 状况8
                setColor(parentOf(x), BLACK);           // 状况8
                setColor(leftOf(sib), BLACK);           // 状况8
                rotateRight(parentOf(x));               // 状况8
                x = root;                               // 状况8
            }
        }
    }
    setColor(x, BLACK);
}

TreeSet

前面已经说过TreeSet是对TeeMap的简单包装,对TreeSet的函数调用都会转换成合适的TeeMap方法,所以TreeSet的实现很是简单。这里再也不赘述。

// TreeSet是对TreeMap的简单包装
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    ......
    private transient NavigableMap<E,Object> m;
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    public TreeSet() {
        this.m = new TreeMap<E,Object>();// TreeSet里面有一个TreeMap
    }
    ......
    public boolean add(E e) {
        return m.put(e, PRESENT)==null;
    }
    ......
}
相关文章
相关标签/搜索