From Map -> HashMap step by step, please feel free to ask

Posted by Trenchant on Thu, 10 Feb 2022 05:52:26 +0100

Author: Mr. SM\
Source: www.cnblogs.com com/king0/p/14176609. html

1, Map

Map 1.1 interface

In Java, Map provides key value mapping. The mapping cannot contain duplicate keys, and each key can only be mapped to one value.

Based on Map key value mapping, Java Util provides HashMap (the most commonly used), TreeMap, Hashtble, LinkedHashMap and other data structures.

Main features of several derived maps:

  • HashMap: the most commonly used data structure. The mapping relationship between key and value is realized by Hash function. When the traversal key is unordered
  • TreeMap: the data structure constructed by using red black tree. Because the principle of red black tree can sort the keys naturally, the keys of TreeMap are arranged in natural order (ascending order) by default.
  • LinkedHashMap: saved the insertion order. The records obtained by traversal are in the insertion order.

1.2 Hash function

Hash (hash function) is to transform the input of any length into the output of fixed length through hash algorithm. The return value of the hash function is also called hash value, hash code digest or hash. The function of hash is shown in the following figure:

Hash function can achieve a better balance in time and space by selecting an appropriate function.

There are two ways to solve Hash: zipper method and linear detection method

1.3 implementation of key value relationship

interface Entry<K,V>

Implementation based on linked list in HashMap

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

It is implemented in the form of tree:

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) {
            super(hash, key, val, next);
        }

1.4 API agreed in map

1.4.1 basic API agreed in map

Basic addition, deletion, modification and query:

int size();  // Return size
boolean isEmpty(); // Is it empty
boolean containsKey(Object key); // Include a key
boolean containsValue(Object value); // Include a value
V get(Object key); // Get the value corresponding to a key
V put(K key, V value); // Stored data
V remove(Object key); // Remove a key
void putAll(Map<? extends K, ? extends V> m); //Insert another set into the set
void clear();  // eliminate
Set<K> keySet(); //Get all the keys of the Map and return to the Set collection
Collection<V> values(); //Returns all values as a Collection
Set<Map.Entry<K, V>> entrySet(); // Map key value pairs to map The internal class Entry implements the mapping relationship. And return all key values mapped to Set set Set.
boolean equals(Object o);
int hashCode(); // Return Hash value
default boolean replace(K key, V oldValue, V newValue); // Alternative operation
default V replace(K key, V value);

1.4.2 advanced API of map agreement

default V getOrDefault(Object key, V defaultValue); //When the acquisition fails, use defaultValue instead.

default void forEach(BiConsumer<? super K, ? super V> action)  // lambda expressions can be used for faster traversal

default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function);

default V putIfAbsent(K key, V value);

default V computeIfAbsent(K key,
            Function<? super K, ? extends V> mappingFunction);

default V computeIfPresent(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction);

default V compute(K key,
            BiFunction<? super K, ? super V, ? extends V> remappingFunction)

default V merge(K key, V value,
            BiFunction<? super V, ? super V, ? extends V> remappingFunction)

1.4.3 use of map advanced API

  • getOrDefault() returns the default value when the value is obtained through the key and the corresponding key or value does not exist, so as to avoid null and program exceptions during use.
  • ForEach() passes in BiConsumer functional interface. The meaning of ForEach() is actually the same as that of Consumer. BiConsumer has an accept method, but BiConsumer has an additional andThen() method. It receives a BiConsumer interface and executes the accept method of this interface first, and then the passed in parameters.
      Map<String, String> map = new HashMap<>();
        map.put("a", "1");
        map.put("b", "2");
        map.put("c", "3");
        map.put("d", "4");
        map.forEach((k, v) -> {
            System.out.println(k+"-"+v);
        });
    }

More function usage:

https://www.cnblogs.com/king0...

1.5 from Map to HashMap

HashMap is an implementation class of Map and the most commonly used implementation class of Map.

1.5.1 inheritance relationship of HashMap

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

In the implementation of HashMap, the method to solve Hash conflict is zipper method. Therefore, in principle, the implementation of HashMap is array + linked list (the entry for the array to save the linked list). When the linked list is too long, in order to optimize the query rate, HashMap converts the linked list into a red black tree (the root node of the array storage tree), so that the query rate is log(n), not O(n) of the linked list.

2, HashMap

/*
 * @author  Doug Lea
 * @author  Josh Bloch
 * @author  Arthur van Hoff
 * @author  Neal Gafter
 * @see     Object#hashCode()
 * @see     Collection
 * @see     Map
 * @see     TreeMap
 * @see     Hashtable
 * @since   1.2
 */

Firstly, HashMap is participated by Doug Lea and Josh Bloch. At the same time, Java's Collections collection system and concurrent framework Doug Lea have also made a lot of contributions.

2.1 basic principles

For an insert operation, first convert the key into the subscript of the array through the Hash function. If the array is empty, directly create nodes and put them into the array. If there are nodes in the array subscript, that is, Hash conflict, use the zipper method to generate a linked list insertion.

Reference picture from https://blog.csdn.net/woshima...

If there is a Hash conflict, use the zipper method to insert. We can insert it at the head of the linked list or at the tail of the linked list. Therefore, the head insertion method is used in JDK 1.7 and the tail insertion method is used in subsequent versions of JDK 1.8.

JDK 1.7 may use header insertion based on the fact that the recently inserted data is the most commonly used, but one of the problems caused by header insertion is that there will be an endless loop in the copy of multi-threaded linked list. Therefore, the tail insertion method adopted after JDK 1.8.

In HashMap, the definition of array + linked list array mentioned earlier

transient Node<K,V>[] table;

Definition of linked list:

static class Node<K,V> implements Map.Entry<K,V>

2.1.2 constructor provided

    public HashMap() { // Empty parameter
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }
    public HashMap(int initialCapacity) { //With the initial size, in general, we need to plan the size of HashMap, because the cost of a capacity expansion operation is very large
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap(int initialCapacity, float loadFactor); // You can customize the load factor public HashMap(int initialCapacity, float loadFactor)// You can customize the load factor

The three constructors do not fully initialize the HashMap. When we insert data for the first time, we allocate heap memory, which improves the response speed of the code.

2.2 definition of Hash function in HashMap

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); // XOR the h upper 16 bits and the lower 16 bits.
    }
// The reason why XOR is used: when two bits are operated, only the probability of 0 and 1 of XOR is the same in and or XOR, and both & and | will bias the result to 0 or 1.

Here you can see that the key of Map can be null, and the hash is a specific value of 0.

The purpose of Hash is to get the subscript of the array table. The goal of Hash function is to evenly distribute the data in the table.

Let's first look at how to get the corresponding array subscript through hash value. The first method: hash% table length(). However, the execution of division operation in CPU is much slower than addition, subtraction and multiplication, and the efficiency is low. The second method, table [(table. Length - 1) & hash], is a subtraction with an operation, which is still faster than division. The constraint here is table length = 2^N.

table.length =16
table.length -1 = 15 1111 1111
//Any number and operation obtains the lower 8 bits of the number, and the other bits are 0

The above example allows us to obtain the corresponding subscript, and (H = key. hashCode()) ^ (H > > > 16) allows Gao 16 to participate in the operation and make full use of the data. Generally, the index of table will not exceed 216, so we directly discard the high-order information, ^ (H > > > 16) allows us to use the high-order information when the amount of data is small. If the index of the table exceeds 216, the Hash obtained by XOR of the high 16 of hashcode () and 16 zeros is also fair.

2.3 HashMap insertion

As we know above, if we get the corresponding table subscript through Hash, so we add the corresponding node to the linked list to complete a Map mapping, indeed jdk1 This is the implementation of HashMap in 7. Let's take a look at the JDK to implement the actual put operation.
Navigate to the put() operation.

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

You can see that the put operation is handed over to putVal for general implementation.

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict);
//onlyIfAbsent if a value already exists in the current position, whether to replace it. false means to replace it, and true means not to replace it
evict // The parameters of the hook function are used in LinkedHashMap and have no meaning in HashMap.

2.3.1 process analysis of putval

In fact, the function of putVal() process is very clear. Here are some key steps to guide.

true calls resizer() to adjust whether to insert for the first time. In fact, at this time, resizer() is fully initialized and then directly assigned to the position of the corresponding index.

 if ((tab = table) == null || (n = tab.length) == 0) // For the first put operation, the tab does not allocate memory. Allocate memory through the resize () method and start working.
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

If the linked list has been converted to a tree, the insertion of the tree is used.

 else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

Traverse each Node in the way of traversal. If the key is the same, or reach the next pointer of the tail Node, insert the data, record the Node position and exit the loop. If the length of the linked list after insertion is 8, call treeifyBin() to check whether to convert the tree.

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

Repeat operation for key: the old value is returned after update, and it also depends on onlyIfAbsent. It is generally true in normal operation and can be ignored.

      if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e); //Hook function for subsequent operations. HashMap is empty without any operation.
                return oldValue;
            }

~

     ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;

Subsequent data maintenance.

2.3.2 meaning of modcount

Fail fast mechanism is an error mechanism in java collection. When multiple threads operate on the contents of the same collection, a fail fast event may be generated. For example, when a thread a traverses a set through the iterator, if the content of the set is changed by other threads; When thread a accesses the collection, it will throw a ConcurrentModificationException and generate a fail fast event. A multi-threaded error checking method to reduce the occurrence of exceptions.

In general, we use ConcurrentHashMap instead of HashMap in multithreaded environment.

2.4 resize() function

HashMap expansion features: the default table size is 16 and the threshold is 12. Load factor LoadFactor. 75, which can be constructed or changed. In the future, the capacity expansion will be doubled.

As for why it is 0.75 code, the reason is also written in the comment. The Poisson distribution model of Hash function is constructed and analyzed.

2.4.1 some predefined parameters of HashMap

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // Aka 16 default size of HashMap. Why use 1 < < 4
static final int MAXIMUM_CAPACITY = 1 << 30; // Maximum capacity
static final float DEFAULT_LOAD_FACTOR = 0.75f; // Loading factor, expansion use

static final int UNTREEIFY_THRESHOLD = 6;//  Threshold value of transforming tree structure into linked list
static final int TREEIFY_THRESHOLD = 8;  //  Threshold value of transforming linked list into tree structure
static final int MIN_TREEIFY_CAPACITY = 64; // If the length of the linked list is greater than 64, the conversion will occur only once before it is converted into an array. This is to avoid unnecessary conversion caused by multiple key value pairs being put into the same linked list at the initial stage of hash table establishment.

// Relevant variables defined
int threshold;   // Threshold indicates that the resize operation will be performed when the size of HashMap is greater than threshold

These variables are related to the capacity expansion mechanism of HashMap and will be used below.

2.4.2 parse of resize() method

      Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0; // The old table length, old table threshold, new table length and new table threshold are defined
  if (oldCap > 0) {  // Data has been inserted, and the parameter is not initialized
            if (oldCap >= MAXIMUM_CAPACITY) {  // If the length of the old table is greater than 1 < < 30;
                threshold = Integer.MAX_VALUE; // threshold sets the maximum value of Integer. That is, we can insert Integer MAX_ Value data
                return oldTab; // Directly return the length of the old table, because the subscript index of the table cannot be expanded.
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && //
                     oldCap >= DEFAULT_INITIAL_CAPACITY)  //The length of the new table is twice that of the old table.
                newThr = oldThr << 1; // double threshold the threshold of the new table is twice that of the old table at the same time
        }
        else if (oldThr > 0) //   This in public HashMap(int initialCapacity, float loadFactor) threshold = tableSizeFor(initialCapacity);   Give the right position
            newCap = oldThr;
        else {               // Zero initial threshold signatures using defaults. If the other two constructors are called, the following code initializes. Because they have not set their threshold, which is 0 by default,
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) { // Correct the threshold. For example, the else if (oldthreshold > 0) section above is not set.
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})

When some parameters are set correctly, capacity expansion starts.

   Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];

When the expansion is completed, the data in the original table will naturally be moved to the new table. The following code completes the task.

if (oldTab != null) {
   for (int j = 0; j < oldCap; ++j) {
      ....
   }
}

How to correctly and quickly expand and adjust the subscript corresponding to each key node? The first method: traverse the node and add it again with put(). This method is implemented, but it is inefficient. Second, we manually assemble the linked list and add it to the corresponding position. Obviously, the second is more efficient than the first, because the first put() has other judgments that do not belong to this situation, such as the judgment of repeated keys.

So JDK 1.8 also uses the second method. We can continue to use e.hash & (newCap - 1) to find the corresponding subscript position. For the old linked list, the operation of e.hash & (newCap - 1) can only produce two different indexes. One keeps the original index unchanged, and the other becomes the original index + oldcap (because the addition of newCap leads to an increase of 1 bit in the number of digits of the index, that is, the leftmost one, and the result of this bit is 1, so it is equivalent to the original index + oldCap). Therefore, if ((e.hash & oldCap) = = 0) can be used to determine whether the index changes.

Therefore, in this way, we can split the original linked list into two new linked lists, and then add them to the corresponding positions. In order to be efficient, we manually assemble the linked list and store it in the corresponding subscript position.

oldCap  = 16
newCap  = 32
hash       : 0001 1011
oldCap-1   : 0000 1111
 The result is     :  0000 1011  Corresponding index 11
-------------------------
e.hash & oldCap Then set at 1,The index needs to be adjusted
oldCap  = 16
hash       : 0001 1011
newCap-1   : 0001 1111
 The result is     :  0001 1011
 Equivalent to 1011 + 1 0000 Original index + newCap
for (int j = 0; j < oldCap; ++j)  // Process each linked list

Handling of special conditions

Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)  // If the linked list has only one node, it is copied directly to the corresponding location, and the subscript is determined by E. hash & (newcap - 1)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode) // If it is a tree, it should be given to the handler of the tree
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

General handling:

       else { // preserve order
                        Node<K,V> loHead = null, loTail = null;  // To build a linked list of the original index position, you need a pointer
                        Node<K,V> hiHead = null, hiTail = null; // Pointer required to build the linked list of the original index + oldCap position
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null); // Divide the original linked list into two linked lists
                        if (loTail != null) { // Write the linked list to the corresponding position
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }

This completes the logic of the resize() method. In general, resizer() completes the complete initialization, memory allocation and subsequent capacity expansion and maintenance of HashMap.

2.5 remove parsing

    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

Leave the remove deletion to the internal function removeNode().

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        Node<K,V>[] tab; Node<K,V> p; int n, index;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (p = tab[index = (n - 1) & hash]) != null) {  // Get index,
            Node<K,V> node = null, e; K k; V v;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k)))) // Determine whether the value at the index is the desired result
                node = p;
            else if ((e = p.next) != null) { // Search algorithm given to tree
                if (p instanceof TreeNode)
                    node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                else {
                    do { // Traversal search
                        if (e.hash == hash &&
                            ((k = e.key) == key ||
                             (key != null && key.equals(k)))) {
                            node = e;
                            break;
                        }
                        p = e;
                    } while ((e = e.next) != null);
                }
            }
            if (node != null && (!matchValue || (v = node.value) == value ||
                                 (value != null && value.equals(v)))) {
                if (node instanceof TreeNode)  //Tree deletion
                    ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
                else if (node == p) // Repair the linked list and delete the linked list
                    tab[index] = node.next;
                else
                    p.next = node.next;
                ++modCount;
                --size;
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

3, Transformation of HashMap from linked list to red black tree

If the length of the linked list (the number of conflicting nodes) has reached 8, treeifyBin() will be called. treeifyBin() will first judge the length of the table of the current hashMap. If it is less than 64, only resize and expand the table. If it reaches 64, the conflicting storage structure will be a red black tree. There is such a field in the source code.

static final int UNTREEIFY_THRESHOLD = 6;
// This shows that the threshold for converting from red black tree to linked list is 6. Why is it not 8?
//If the insertion and deletion are around 8, converting them into each other will waste a lot of time and affect its performance.
//If the operation of the transformation between the two is unbalanced and biased to one side, such effects can be avoided.

3.1 data structure of red black tree

    static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // After deletion, you need to unlink and point to the previous node (the previous node in the original linked list)
        boolean red;
    }

Because it inherits LinkedHashMap Entry < K, V >, so the stored data is still in the entry:

   static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
   }

3.2 treeifyBin()

Treeifybin () determines when a linked list is transformed into a red black tree. treeifyBin() has two formats:

final void treeifyBin(Node<K,V>[] tab, int hash);
 final void treeify(Node<K,V>[] tab);
    final void treeifyBin(Node<K,V>[] tab, int hash) { // The simple Node is modified to TreeNode, and the prev attribute is maintained at the same time.
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null);
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);  // Really generate red and black trees
        }
    }
    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        return new TreeNode<>(p.hash, p.key, p.value, next);
    } // Realize the transformation from Node linked list Node to TreeNode Node.

The following function really realizes the transformation of the red and black tree of the linked list. First, a standard query two fork tree is constructed, then the two fork tree is querying in a standard way and then adjusted to a red black tree. The balanceInsertion() implements the adjustment.

        /**
         * Forms tree of the nodes linked from this node.
         */
        final void treeify(Node<K,V>[] tab) {
            TreeNode<K,V> root = null;
            for (TreeNode<K,V> x = this, next; x != null; x = next) {
                next = (TreeNode<K,V>)x.next;
                x.left = x.right = null;
                if (root == null) { // In the first conversion process, the head node of the linked list is taken as the root node.
                    x.parent = null;
                    x.red = false;  // The definition root node of red black tree must be black
                    root = x;
                }
                else {
                    K k = x.key;
                    int h = x.hash;
                    Class<?> kc = null;
                    for (TreeNode<K,V> p = root;;) {
                        int dir, ph;
                        K pk = p.key;
                        if ((ph = p.hash) > h)  ////The insertion order is determined by the size of the Hash
                            dir = -1; // Identification of dir size order
                        else if (ph < h)
                            dir = 1;
                        else if ((kc == null && //When the values of two hashes are the same, a special method is used to determine the size.
                                  (kc = comparableClassFor(k)) == null) || // Returns x's Class if it is of the form "class C implements Comparable ", else null.  If the source code writing format of the key class is C implementation comparable < C >, the type C of this class will be returned. If it is implemented indirectly, it will not work. If it is a String type, directly return String class
                                 (dir = compareComparables(kc, k, pk)) == 0)   //    ((Comparable)k).compareTo(pk));  Compare after forced conversion. If dir == 0, tiebreakhorder() will continue arbitration
                            dir = tieBreakOrder(k, pk);  // First, compare the class types of the two. If they are equal, use (system. Identityhashcode (a) < = system Identityhashcode (b) uses the original hashcode, not rewritten in comparison.

                        TreeNode<K,V> xp = p; // Traversal, previous node
                        if ((p = (dir <= 0) ? p.left : p.right) == null) { //Through dir, look down p until p is null and find an insertion time
                            x.parent = xp;
                            if (dir <= 0)
                                xp.left = x;
                            else
                                xp.right = x;
                            root = balanceInsertion(root, x); //Adjust the binary tree
                            break;
                        }
                    }
                }
            }
            moveRootToFront(tab, root);
        }

3.3 operation of transforming a binary tree into a red black tree - balanceInsertion()

When adding new nodes in the red black tree, you need to call the balanceInsertion method to ensure the characteristics of the red black tree.

If you want to understand the insertion process of red black tree, you must have a clear understanding of the nature of red black tree.

Properties of red black tree:

  1. Each node is either red or black
  2. The root node is black
  3. Each leaf node (NIL) is black
  4. If a node is red, its two sons are black.
  5. For each node, the number of black nodes on all paths from this node to its leaf node is the same.
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root,
                                                    TreeNode<K,V> x) {
            x.red = true;  // The inserted child node must be red
            for (TreeNode<K,V> xp, xpp, xppl, xppr;;) { ////x current processing node xp parent node xpp grandfather node xppl grandfather left node xppr grandfather right node
                if ((xp = x.parent) == null) { // If the current processing node is the root node and meets the nature of red black tree, the cycle is ended
                    x.red = false;
                    return x;
                }
                else if (!xp.red || (xpp = xp.parent) == null)
                    return root;
                if (xp == (xppl = xpp.left)) {
                    if ((xppr = xpp.right) != null && xppr.red) {
                        xppr.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
                        if (x == xp.right) {
                            root = rotateLeft(root, x = xp);
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateRight(root, xpp);
                            }
                        }
                    }
                }
                else {
                    if (xppl != null && xppl.red) {
                        xppl.red = false;
                        xp.red = false;
                        xpp.red = true;
                        x = xpp;
                    }
                    else {
                        if (x == xp.left) {
                            root = rotateRight(root, x = xp);
                            xpp = (xp = x.parent) == null ? null : xp.parent;
                        }
                        if (xp != null) {
                            xp.red = false;
                            if (xpp != null) {
                                xpp.red = true;
                                root = rotateLeft(root, xpp);
                            }
                        }
                    }
                }
            }
        }

TreeNode red black tree summary

TreeNode completely implements a set of rules for adding, deleting, modifying and querying red and black trees. The implementation refers to the introduction to algorithm

/* ------------------------------------------------------------ */
// Red-black tree methods, all adapted from CLR

Here we recommend a red and black tree animation demonstration website https://rbtree.phpisfuture.com/
Red black tree is a non strict balanced binary search tree, which is highly similar to log(N).

4, Extension of HashMap

A property of key in Map is that key cannot be repeated, while the meaning of Java Set: there cannot be repeated elements in the Set. The implementation of HashMap is excellent enough. So can we use the property of key to implement Set? Indeed, HashSet in JDK does just that.

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    private transient HashMap<E,Object> map;
      // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

}

PRESENT is the value stored in the Map, and key is the implementation of Set semantics. Moreover, it can be judged that Null values are allowed to be stored in HashSet.

Recent hot article recommendations:

1.1000 + Java interview questions and answers (2022 latest version)

2.Hot! The Java collaboration is coming...

3.Spring Boot 2.x tutorial, too complete!

4.20w programmer red envelope cover, get it quickly...

5.Java development manual (Songshan version) is the latest release. Download it quickly!

Feel good, don't forget to like + forward!

Topics: Java