The clearest explanation of red and black trees in history (I)

Posted by shah on Sat, 08 Jun 2019 23:17:04 +0200

The github address of this article

This paper takes Java TreeMap as an example to illustrate the insertion, deletion and adjustment process of Red-Black tree from the source code level with detailed illustrations.

Overall introduction

Java TreeMap implements the SortedMap interface, which means that elements in the Map are sorted according to the size of the key. The key size can be judged by its natural order (natural order). ordering can also be constructed by passing in a Comparator.

The bottom layer of TreeMap is implemented by Red-Black tree, which means that both containsKey(), get(), put(), remove() have log(n) time complexity. The implementation of the algorithm refers to Introduction to Algorithms.

For performance reasons, TreeMap is not synchronized, requiring programmers to synchronize manually if they need to be used in a multi-threaded environment, or wrapping TreeMap into wrapped synchronization as follows:

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

Red-black tree is an approximate balanced binary search tree, which can ensure that the height difference between left and right subtrees of any node does not exceed that of the lower one. Specifically, a red-black tree is a binary search tree that satisfies the following conditions:

  1. Each node is either red or black.
  2. The root node must be black
  3. Red nodes cannot be continuous (that is, neither children nor fathers of red nodes can be red).
  4. For each node, any path from that point to null (the end of the tree) contains the same number of black nodes.

When the structure of the tree changes (insertion or deletion operations), the above conditions 3 or 4 are often destroyed, and the search tree needs to be adjusted to meet the conditions of the red-black tree again.

Preparatory knowledge

As mentioned in the previous article, when the structure of the search tree changes, the condition of the red-black tree may be destroyed. It needs to be adjusted to make the search tree meet the condition of the red-black tree again. Adjustment can be divided into two categories: one is color adjustment, that is to change the color of a node; the other is structure adjustment, which sets the structural relationship of the search tree. The structural adjustment process consists of two basic operations: Rotate Left and Rotate Right.

Levo

The left-handed process is to rotate the right subtree of X around x counterclockwise, making the right subtree of x the father of x, and modifying the reference of the relevant nodes. After rotation, the properties of the binary search tree are still satisfied.

The left-handed code in TreeMap is as follows:

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

Dextral rotation

The right-handed process is to rotate the left subtree of X clockwise around x, making the left subtree of x the father of x, and modifying the reference of related nodes. After rotation, the properties of the binary search tree are still satisfied.

The right-handed code in TreeMap is as follows:

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

Method Analysis

get()

get(Object The key method returns the corresponding value based on the specified key value, which calls getEntry(Object). Key) Gets the corresponding entry, and then returns entry.value. So getEntry() is the core of the algorithm. The idea of the algorithm is to search the binary search tree according to the natural order of keys (or comparator order) until the k.compareTo(p.key) is satisfied. == 0 entry.

The code is as follows:

//getEntry() method
final Entry<K,V> getEntry(Object key) {
    ......
    if (key == null)//No key value is allowed to be null
        throw new NullPointerException();
    Comparable<? super K> k = (Comparable<? super K>) key;//Natural order of using elements
    Entry<K,V> p = root;
    while (p != null) {
        int cmp = k.compareTo(p.key);
        if (cmp < 0)//Look left
            p = p.left;
        else if (cmp > 0)//Look to the right
            p = p.right;
        else
            return p;
    }
    return null;
}

put()

put(K The key, V value) method is to add the specified key, value pair to the map. This method first looks up the map to see if it contains the tuple, and returns directly if it already contains it. The search process is similar to the getEntry() method. If not found, a new entry will be inserted into the red-black tree. If the constraint of the red-black tree is broken after insertion, adjustments will be needed (rotation, change the color of some nodes).

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;//Natural order of using elements
    do {
        parent = t;
        cmp = k.compareTo(t.key);
        if (cmp < 0) t = t.left;//Look left
        else if (cmp > 0) t = t.right;//Look to the right
        else return t.setValue(value);
    } while (t != null);
    Entry<K,V> e = new Entry<>(key, value, parent);//Create and insert a new entry
    if (cmp < 0) parent.left = e;
    else parent.right = e;
    fixAfterInsertion(e);//adjustment
    size++;
    return null;
}

The insertion part of the above code is not difficult to understand: first, find the right place on the red-black tree, then create a new entry and insert it (of course, the newly inserted node must be the leaf of the tree). The difficulty is the adjustment function fixAfterInsertion(). As mentioned earlier, adjustment often requires 1. changing the color of some nodes and 2. rotating some nodes.

The specific code for the adjustment function fixAfterInsertion() is as follows, using the rotateLeft() and rotateRight() functions mentioned above. From the code, we can see that case 2 actually falls within case 3. The first three cases are symmetrical, so the last three cases are not drawn in the diagram. The reader can understand them by referring to the code.

//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) {//If y is null, it is considered BLACK.
                setColor(parentOf(x), BLACK);              // Situation 1
                setColor(y, BLACK);                        // Situation 1
                setColor(parentOf(parentOf(x)), RED);      // Situation 1
                x = parentOf(parentOf(x));                 // Situation 1
            } else {
                if (x == rightOf(parentOf(x))) {
                    x = parentOf(x);                       // Situation 2
                    rotateLeft(x);                         // Situation 2
                }
                setColor(parentOf(x), BLACK);              // Situation 3
                setColor(parentOf(parentOf(x)), RED);      // Situation 3
                rotateRight(parentOf(parentOf(x)));        // Situation 3
            }
        } else {
            Entry<K,V> y = leftOf(parentOf(parentOf(x)));
            if (colorOf(y) == RED) {
                setColor(parentOf(x), BLACK);              // Situation 4
                setColor(y, BLACK);                        // Situation 4
                setColor(parentOf(parentOf(x)), RED);      // Situation 4
                x = parentOf(parentOf(x));                 // Situation 4
            } else {
                if (x == leftOf(parentOf(x))) {
                    x = parentOf(x);                       // Situation 5
                    rotateRight(x);                        // Situation 5
                }
                setColor(parentOf(x), BLACK);              // Situation 6
                setColor(parentOf(parentOf(x)), RED);      // Situation 6
                rotateLeft(parentOf(parentOf(x)));         // Situation 6
            }
        }
    }
    root.color = BLACK;
}

remove()

remove(Object The function of key is to delete entry corresponding to the key value. This method first passes getEntry(Object) mentioned above. The key method finds the entry corresponding to the key value, and then calls deleteEntry (Entry < K, V > entry) Delete the corresponding entry. Because deletion can change the structure of the red-black tree, it may break the constraints of the red-black tree, so it may need to be adjusted.

Detailed explanations on remove() will be included in the next blog post, please look forward to it!

Topics: Java github