11.3 红黑树

1 红黑树性质

红黑树是针对排序二叉树在最坏情况下会变成普通链表导致检索效率慢而改进的一种排序二叉树。

注:排序二叉树可以快速检索,只是在最坏的情况下,比如插入的节点集本身是有序的(由大到小,或由小到大排列),那么最后得到的排序二叉树将变成链表:所有节点只有左节点(节点集由大到小排列),或者所有节点只有右节点(节点集由小到大排列),此时排序二叉树就变成了普通链表,其检索效率会很差。

红黑树是一种自平衡二叉查找树,在进行插入和删除节点操作时通过特定操作保持二叉查找树的平衡,从而保证较高的查找性能。

红黑树是一个更高效的检索二叉树,其在最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除。

其典型的应用是实现关联数组,JDK 中的集合类 TreeMap、TreeSet(基于TreeMap) 就是红黑树的实现。

红黑树在排序二叉树的基础上增加了如下性质:

  1. 每个节点要么是红色,要么是黑色;
  2. 根节点永远是黑色的;
  3. 所有的叶子节点都是空节点,并且是黑色的;
  4. 每个红色节点的两个子节点都是黑色,从任一叶子到根节点的路径上不会有连续的红色节点
  5. 从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点;

性质3中指定所有的叶子节点都是空节点,并且是黑色的,Java 实现中以 null 来表示空节点。

Java 实现的红黑树可能的结构图如下:
这里写图片描述

从根节点到叶子节点的路径中包含的黑色节点数被称为“黑色高度(black-height)”。

性质4保证了从根节点到叶子节点的最长路径的长度不会超过任何其他路径的2倍。
假如一颗黑色高度为3的红黑树,从根节点到叶子节点的最短路径长度是2,该路径上全是黑色节点(黑 - 黑 - 黑);最长路径也只可能为4,即在每个黑色节点之间有一个红色节点(黑 - 红 - 黑 - 红 - 黑)。
该性质保证不能插入更多的红色节点,那么,红黑树中最长的路径就是一条红黑交替的路径。

由此可以得出结论:对于黑色高度为N的红黑树,从根到叶子节点的最短路径长度为 N - 1,最长路径长度为 2*(N - 1)。

由于红黑树只是一个特殊的排序二叉树,其只读操作和普通排序二叉树的只读操作性能相同,但红黑树保持了大致平衡,所以其检索性能更好。

在红黑树上进行元素的插入和删除操作会破坏红黑树的特征,因此对这两种操作都要进行后续的维护处理,以保证操作后的树依然是红黑树。

1.1 插入节点

插入节点的步骤如下:

(1) 以排序二叉树的方法插入新节点,并设为红色;

      若新节点设置为黑色,会导致根节点到叶子节点的路径上多出一个黑节点,这样会很难调整;而设为红色节点,可能会出现两个连续的红色节点,通过颜色调换和树旋转调整即可。

(2) 颜色调换和树旋转

      颜色调换和树旋转比较复杂,分多种情况。

      下文所涉及到的字母定义如下:
      N: 新插入的节点
      P: N节点的父节点
      U: P节点的兄弟节点
      G: P节点的父节点

1)新节点是根节点

      根据性质2,根节点设置为黑色。

2)新节点有父节点,且为黑色

      此时,新插入的节点是红色的,依然满足性质 4;另外新节点 N 有两个黑色叶子节点(null 节点),由于新节点 N 是红色,通过它的每个子节点的路径依然保持相同的黑色节点数,因此依然满足性质 5。

3)父节点和父节点的兄弟节点 U 都是红色

      此时,需将 P 节点、U 节点都设置为黑色,并将 P 节点的父节点设为红色(用来保持性质 5)。现在新节点 N 有了一个黑色的父节点 P,由于从 P 节点、U 节点到根节点的任何路径都必须通过 G 节点,在这些路径上的黑节点数目没有改变(原来有叶子和 G 节点两个黑色节点,现在有叶子和 P 两个黑色节点)。

      经过处理后,红色的 G 节点的父节点也有可能是红色的,这就违反了性质 4,因此还需要对 G 节点递归地进行上述过程(即把 G 当成新插入的节点进行处理)。

      处理过程如下图(无论N节点是作为P节点的左子节点还是右子节点):
这里写图片描述

4)新节点作为父节点P的右子节点,而父节点P是红色,其兄弟节点U是黑色或缺失,P为其父节点G的左子节点

      此时,需对新节点和其父节点进行一次左旋转,接着按情形 3 处理父节点 P(把 P 当成新插入的节点)。这导致某些路径通过它们以前不通过的新节点 N 或父节点 P 的其中之一,但是这两个节点都是红色的,因此不会影响性质 5。

      处理过程如下图:
这里写图片描述
注:图中 P 节点是 G 节点的左子节点,如果 P 节点是其父节点 G 节点的右子节点,上面的处理情况需左、右对调一下。

疑问:这样处理不满足性质4啊(每个红色节点的两个子节点都是黑色,从任一叶子到根节点的路径上不会有连续的红色节点),现在N和P是连续的红色节点。

5)新节点作为父节点P的左子节点,而父节点P是红色,其兄弟节点U是黑色或缺失,P为其父节点G的左子节点

      此时,需要对节点 G 进行一次右旋转,旋转后,之前的父节点 P 现在是新节点 N 和节点 G 的父节点。由于之前的节点 G 是黑色,我们切换父节点 P 和节点 G 的颜色,使之满足性质 4,性质 5 也仍然保持满足,因为通过这三个节点中任何一个的所有路径以前都通过节点 G,现在它们都通过父节点 P。在各自的情形下,这都是三个节点中唯一的黑色节点。

      处理过程如下图:
这里写图片描述
注:图 中 P 节点是 G 节点的左子节点,如果 P 节点是右子节点,上面的处理情况需左、右对调一下。

1.2 删除节点

红黑树的删除操作比插入操作要复杂一些,步骤如下:

(1) 以排序二叉树的方法删除指定节点;

(2) 进行颜色调换和树旋转,以满足红黑树特性。

下面以 TreeMap 的实现说明红黑树的插入和删除操作原理。

2 TreeMap 实现原理

TreeMap 中使用 Entry 内部静态类表示节点:

/**
 * Node in the Tree.  Doubles as a means to pass key-value pairs back to user (see Map.Entry).
 */
static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;

    /**
     * Make a new cell with given key, value, and parent, and with
     * {@code null} child links, and BLACK color.
     */
    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }

    /**
     * Returns the key.
     */
    public K getKey() {
        return key;
    }

    /**
     * Returns the value associated with the key.
     */
    public V getValue() {
        return value;
    }

    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }

    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }

    public String toString() {
        return key + "=" + value;
    }
}

2.1 添加元素

TreeMap 的 put(K key, V value) 方法实现了将节点放入排序二叉树中,其源代码如下:

/**
 * comparator 用于维护 tree map 节点顺序, 或为 null ,如果使用 key 本身的自然顺序
 */
private final Comparator<? super K> comparator;
// 根节点
// root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
                               it, str, defaultVal);
private transient Entry<K,V> root;

public V put(K key, V value) {
   Entry<K,V> t = root;
   if (t == null) {
       compare(key, key); // type (and possibly null) check
       // 以新的 key-value 创建一个 Entry
       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;
   // 如果 comparator 不为 null,则通过比较key排序
   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;
}

从put方法可知,当添加新节点时,程序从根节点开始比较 ,如果新增节点大于当前节点、并且当前节点的右子节点存在,则以右子节点作为当前节点;如果新增节点小于当前节点、并且当前节点的左子节点存在,则以左子节点作为当前节点;如果新增节点等于当前节点,则用新增节点覆盖当前节点,并结束循环。

直到找到某个节点的左、右子节点不存在,将新节点添加该节点的子节点 ,如果新节点比该节点大,则添加为右子节点;如果新节点比该节点小,则添加为左子节点。

插入节点后,修复红黑树的 fixAfterInsertion(e) 方法如下:

/** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {
    x.color = RED; // 插入的节点为红色

    while (x != null && x != root && x.parent.color == RED) {
        // 如果 x 的父节点是其父节点的左子节点
        if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
            // 获取 x 的父节点的父节点的右子节点(即x 的父节点的右兄弟节点)
            Entry<K,V> y = rightOf(parentOf(parentOf(x)));
            // 如果 x 的父节点的右兄弟节点为红色
            if (colorOf(y) == RED) {
                // 将 x 的父节点设为黑色
                setColor(parentOf(x), BLACK);
                // 将 x 的父节点的右兄弟节点设为黑色
                setColor(y, BLACK);
                // 将 x 的父节点的父节点设为红色
                setColor(parentOf(parentOf(x)), RED);
                // 将 x 的父节点的父节点作为新节点进行递归处理
                x = parentOf(parentOf(x));
            } else {
                // 如果 x 是其父节点的右子节点
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);
                    // 将 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;
}

2.2 删除节点

TreeMap 删除节点的方法 deleteEntry() 如下:

/**
 * 删除P节点, 然后使树重新平衡.
 */
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;
        }
    }
}

删除节点后,修复红黑树的 fixAfterDeletion(p) 方法如下:

/** 删除节点后修复红黑树 */
private void fixAfterDeletion(Entry<K,V> x) {
    // 直到 x 不是根节点,且 x 的颜色是黑色
    while (x != root && colorOf(x) == BLACK) {
        // 如果 x 是其父节点的左子节点
        if (x == leftOf(parentOf(x))) {
            // 获取 x 节点的右兄弟节点
            Entry<K,V> sib = rightOf(parentOf(x));
            // 如果 x 节点的右兄弟节点是红色
            if (colorOf(sib) == RED) {
                setColor(sib, BLACK);
                // 将 x 的父节点设为红色
                setColor(parentOf(x), RED);
                // 将 x 的父节点左旋转
                rotateLeft(parentOf(x));
                // 再次将 sib 设为 x 的父节点的右子节点
                sib = rightOf(parentOf(x));
            }

            // 如果 sib 的两个子节点都是黑色
            if (colorOf(leftOf(sib))  == BLACK &&
                colorOf(rightOf(sib)) == BLACK) {
                // sib 设置为红色
                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 对称的(如果 x 是其父节点的右子节点)
            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);
}



参考资料:

疯狂Java:突破程序员基本功的16课-红黑树树(第11课)

猜你喜欢

转载自blog.csdn.net/zhouxukun123/article/details/79835598