Java8 HashMap源码阅读

二叉查找树和红黑树

二叉查找树

二叉查找树满足对于任意节点N,其左子树的所有节点的值都小于节点N,右子树的所有节点的值都大于节点N。树的高度为[log2N,N]。查找、插入、删除的时间复杂度最优为O(log2N),最坏为O(N)

红黑树

红黑树性质:

  1. 每个节点要么是红色,要么是黑色
  2. 根节点是黑色的,null节点是黑色
  3. 如果一个节点是红色的,则它的子节点必须是黑色的
  4. 从一个节点到到null引用的每一条路径必须包含相同数目的黑节点

红黑树的高度最大为2log2(N+1)。查找、插入、删除时间复杂度为O(log2N)

HashMap put 方法

 

HashMap内部通过数组加链表的方式保存数据。

 

添加节点(键值对)到HashMap:

 

  1. 首先通过 (n - 1) & hash 得到数组索引位置 j,其中 n 为数组长度,hash为键值对中键的哈希值;
  2. 如果 j 处无节点,则把此节点添加到数组的 j 位置,为链表的根节点;
  3. 如果 j 处有节点,而且链表不为红黑树结构,则把节点插到 j 处链表的末尾(假设此节点的key在HashMap中不存在),链表插入时间复杂度为O(N);若链表的长度大于某个值(默认为8),而且数组的长度大于某个值(默认为64),把此链表转化为红黑树结构
  4. 如果 j 处有节点,而且链表为红黑树结构,则把节点插入红黑树结构中,链表插入时间复杂度为O(log2N)

 

基本哈希表节点,HashMap保存的的数据的基本类型:

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

当为红黑树结构时保存的数据类型,TreeNode是上面代码Node的子类,super也即调用的Node的方法。prev字段使链表变成双向链表;red为true表示此节点颜色为红色,否则为黑色。

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

HashMap put方法,调用putVal

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

 putVal方法,部分代码,1 判断数组第一个节点是否和插入数据的key相等,若相等,则把数组第一个节点赋值给e,后面会用新value覆盖e的value。2 如果此链表节点类型为TreeNode,即红黑树结构,则调用TreeNode putTreeVal方法。3 否则遍历节点,如果遍历的节点和要插入数据的key相等,则把此节点赋值给e,否则创建一个新节点并插入链表尾端。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
        // 判断第一个节点的key是否和插入的key相等
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
     // 把节点插入到红黑树结构中
        else if (p instanceof TreeNode)
            e = ((HashMap.TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
        else {
       // 遍历链表,把节点插入到链表尾端
            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;
            }
        }
    }
}            

TreeNode putTreeVal方法,部分代码,把节点插入红黑树中。1 通过二叉查找树左子树小于父节点、父节点小于右子树的性质遍历到叶子节点,即为null的节点,把新节点插入到此位置。2 调用balanceInserting方法使树结构重新恢复为红黑树结构

final HashMap.TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                       int h, K k, V v) {
    // 找根节点
    HashMap.TreeNode<K,V> root = (parent != null) ? root() : this;
    // 在红黑树中查找要插入的位置
    for (HashMap.TreeNode<K,V> p = root;;) {
        int dir, ph; K pk;
        if ((ph = p.hash) > h)
            dir = -1;
        else if (ph < h)
            dir = 1;
        else if ((pk = p.key) == k || (k != null && k.equals(pk)))
            return p;

        HashMap.TreeNode<K,V> xp = p;
        // 当找到插入的位置时,插入位置为空节点
        if ((p = (dir <= 0) ? p.left : p.right) == null) {
            Node<K,V> xpn = xp.next;
            HashMap.TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
            if (dir <= 0)
                xp.left = x;
            else
                xp.right = x;
            xp.next = x;
            x.parent = x.prev = xp;
            if (xpn != null)
                ((HashMap.TreeNode<K,V>)xpn).prev = x;
            // 插入新节点x后,可能会破坏红黑树的性质,调用balanceInsertion恢复红黑树结构
            moveRootToFront(tab, balanceInsertion(root, x));
            return null;
        }
    }
}

TreeNode balanceInserting方法,部分代码。修复红黑树结构参考文章https://tech.meituan.com/2016/12/02/redblack-tree.html

static <K,V> HashMap.TreeNode<K,V> balanceInsertion(HashMap.TreeNode<K,V> root,
                                                    HashMap.TreeNode<K,V> x) {
    // 插入节点为红色
    x.red = true;
    for (HashMap.TreeNode<K,V> xp, xpp, xppl, xppr;;) {
        // 插入节点为根节点
        if ((xp = x.parent) == null) {
            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 {
                // 父节点为红色,父节点的兄弟节点为黑色
                // 如果节点X,父节点,祖父节点不再一条直线上,左旋使他们在一条直线上
                if (x == xp.right) {
                    root = rotateLeft(root, x = xp);
                    xpp = (xp = x.parent) == null ? null : xp.parent;
                }
                // 父节点为红色,父节点的兄弟节点为黑色
                // 节点X,父节点,祖父节点在一条直线上,右旋
                if (xp != null) {
                    xp.red = false;
                    if (xpp != null) {
                        xpp.red = true;
                        root = rotateRight(root, xpp);
                    }
                }
            }
        }
        // 如果父节点为有孩子,是父节点为左孩子时的镜像操作
        else {
        }
    }
}

 

若有理解错误的、写错的地方、更好的思路,方法,望各位读者评论指正或指点我,不胜感激

本文主要参考以下文章:

https://tech.meituan.com/2016/12/02/redblack-tree.html

https://www.cnblogs.com/gaochundong/p/binary_search_tree.html#delete-node

posted @ 2019-08-20 11:24  寄予心  阅读(98)  评论(0)    收藏  举报