HashMap 及其类似数据结构 原理分析对比

2018-08-10  本文已影响0人  FelixLiuu

HashMap简述

HashMap 是数组+ 链表 的数据结构。时间复杂度(理想状态)O(1)。(是数组 查找时间复杂度O(1),插入时间复杂度O(n),链表 查找时间复杂度O(n),插入时间复杂度O(1) 的 中庸选择)

  • 链表使用一个next指针维护链表结构的,它的插入和删除的效率比较高,再插入和删除时,不用挪动后面的数据。但是查找每次都得从头结点遍历,所以效率不高
  • 数组使用下标维护数据的,所以查找起来,效率会很高。插入和删除,需要移动后面的数据,效率不高。
  • 所以,在只需要查找的时候,建议使用数组,而经常需要插入和删除数据,建议使用链表

HashMap主要原理

1、put

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

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
           boolean evict) {

        Node<K,V>[] tab; Node<K,V> p; int n, i;

    // 1. 若哈希表的数组tab为空,则 通过resize() 创建
    // 所以,初始化哈希表的时机 = 第1次调用put函数时,即调用resize() 初始化创建
    // 关于resize()的源码分析将在下面讲解扩容时详细分析,此处先跳过
    if ((tab = table) == null || (n = tab.length) == 0)
    n = (tab = resize()).length;

    // 2. 计算插入存储的数组索引i:根据键值key计算的hash值 得到
    // 此处的数组下标计算方式 = i = (n - 1) & hash,同JDK 1.7中的indexFor(),上面已详细描述

    // 3. 插入时,需判断是否存在Hash冲突:
    // 若不存在(即当前table[i] == null),则直接在该数组位置新建节点,插入完毕
    // 否则,代表存在Hash冲突,即当前存储位置已存在节点,则依次往下判断:a. 当前位置的key是否与需插入的key相同、b. 判断需插入的数据结构是否为红黑树 or 链表
    if ((p = tab[i = (n - 1) & hash]) == null)
    tab[i] = newNode(hash, key, value, null);  // newNode(hash, key, value, null)的源码 = new Node<>(hash, key, value, next)

else {
    Node<K,V> e; K k;

    // a. 判断 table[i]的元素的key是否与 需插入的key一样,若相同则 直接用新value 覆盖 旧value
    // 判断原则:equals()
    if (p.hash == hash &&
        ((k = p.key) == key || (key != null && key.equals(k))))
        e = p;

    // b. 继续判断:需插入的数据结构是否为红黑树 or 链表
    // 若是红黑树,则直接在树中插入 or 更新键值对
    else if (p instanceof TreeNode)
        e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); ->>分析3

    // 若是链表,则在链表中插入 or 更新键值对
    // i.  遍历table[i],判断Key是否已存在:采用equals() 对比当前遍历节点的key 与 需插入数据的key:若已存在,则直接用新value 覆盖 旧value
    // ii. 遍历完毕后仍无发现上述情况,则直接在链表尾部插入数据
    // 注:新增节点后,需判断链表长度是否>8(8 = 桶的树化阈值):若是,则把链表转换为红黑树
    
    else {
        for (int binCount = 0; ; ++binCount) {
            // 对于ii:若数组的下1个位置,表示已到表尾也没有找到key值相同节点,则新建节点 = 插入节点
            // 注:此处是从链表尾插入,与JDK 1.7不同(从链表头插入,即永远都是添加到数组的位置,原来数组位置的数据则往后移)
            if ((e = p.next) == null) {
                p.next = newNode(hash, key, value, null);

                // 插入节点后,若链表节点>数阈值,则将链表转换为红黑树
                if (binCount >= TREEIFY_THRESHOLD - 1) 
                    treeifyBin(tab, hash); // 树化操作
                break;
            }

            // 对于i
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                break;

            // 更新p指向下一个节点,继续遍历
            p = e;
        }
    }

    // 对i情况的后续操作:发现key已存在,直接用新value 覆盖 旧value & 返回旧value
    if (e != null) { 
        V oldValue = e.value;
        if (!onlyIfAbsent || oldValue == null)
            e.value = value;
        afterNodeAccess(e); // 替换旧值时会调用的方法(默认实现为空)
        return oldValue;
    }
}

++modCount;

// 插入成功后,判断实际存在的键值对数量size > 最大容量threshold
if (++size > threshold)
    resize();

afterNodeInsertion(evict);// 插入成功时会调用的方法(默认实现为空)
return null;

}

2、get

public V get(Object key) {
    Node<K,V> e;
    // 1. 计算需获取数据的hash值
    // 2. 通过getNode()获取所查询的数据
    // 3. 获取后,判断数据是否为空
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

 
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;

    // 1. 计算存放在数组table中的位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
    (first = tab[(n - 1) & hash]) != null) {

    // 4. 通过该函数,依次在数组、红黑树、链表中查找(通过equals()判断)
    // a. 先在数组中找,若存在,则直接返回
    if (first.hash == hash && // always check first node
        ((k = first.key) == key || (key != null && key.equals(k))))
        return first;

    // b. 若数组中没有,则到红黑树中寻找
    if ((e = first.next) != null) {
        // 在树中get
        if (first instanceof TreeNode)
            return ((TreeNode<K,V>)first).getTreeNode(hash, key);

        // c. 若红黑树中也没有,则通过遍历,到链表中寻找
        do {
            if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                return e;
        } while ((e = e.next) != null);
    }
}
    return null;
}

3、resize
存储键值时,发现容量不足,需要扩容,生成一个当前 2倍 的新数组。

/**
 * 分析4:resize()
 * 该函数有2种使用情况:1.初始化哈希表 2.当前数组容量过小,需扩容
 */
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table; // 扩容前的数组(当前数组)
int oldCap = (oldTab == null) ? 0 : oldTab.length; // 扩容前的数组的容量 = 长度
int oldThr = threshold;// 扩容前的数组的阈值
int newCap, newThr = 0;

// 针对情况2:若扩容前的数组容量超过最大值,则不再扩充
if (oldCap > 0) {
    if (oldCap >= MAXIMUM_CAPACITY) {
        threshold = Integer.MAX_VALUE;
        return oldTab;
    }

    // 针对情况2:若无超过最大值,就扩充为原来的2倍
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // 通过右移扩充2倍
}

// 针对情况1:初始化哈希表(采用指定 or 默认值)
else if (oldThr > 0) // initial capacity was placed in threshold
    newCap = oldThr;

else {               // zero initial threshold signifies using defaults
    newCap = DEFAULT_INITIAL_CAPACITY;
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}

// 计算新的resize上限
if (newThr == 0) {
    float ft = (float)newCap * loadFactor;
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);
}

threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

if (oldTab != null) {
    // 把每个bucket都移动到新的buckets中
    for (int j = 0; j < oldCap; ++j) {
        Node<K,V> e;
        if ((e = oldTab[j]) != null) {
            oldTab[j] = null;

            if (e.next == null)
                newTab[e.hash & (newCap - 1)] = e;
            else if (e instanceof TreeNode)
                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

            else { // 链表优化重hash的代码块
                Node<K,V> loHead = null, loTail = null;
                Node<K,V> hiHead = null, hiTail = null;
                Node<K,V> next;
                do {
                    next = e.next;
                    // 原索引
                    if ((e.hash & oldCap) == 0) {
                        if (loTail == null)
                            loHead = e;
                        else
                            loTail.next = e;
                        loTail = e;
                    }
                    // 原索引 + oldCap
                    else {
                        if (hiTail == null)
                            hiHead = e;
                        else
                            hiTail.next = e;
                        hiTail = e;
                    }
                } while ((e = next) != null);
                // 原索引放到bucket里
                if (loTail != null) {
                    loTail.next = null;
                    newTab[j] = loHead;
                }
                // 原索引+oldCap放到bucket里
                if (hiTail != null) {
                    hiTail.next = null;
                    newTab[j + oldCap] = hiHead;
                }
            }
        }
    }
}
    return newTab;
}

总结

HashMap 采用数组和链表的方式解决hash冲突,(Java 8 添加红黑树)。允许存储null。

hashmap流程图

参考:

动态扩容问题详见: HashMap扩容

扩展

LinkedHashMap

基本和HashMap实现类似,多了一个链表来维护元素插入的顺序,因此维护的效率会比HashMap略低。但是因为有链表的存在,遍历效率会高于HashMap。
get()操作后,会把当前数据移动到链表尾部。所以基于这个属性,可以实现 Lru缓存

详见:

ConCurrentHashMap

结构图

线程安全,而且采用分段锁的方式进行数据同步,因此相对于Hashtable来说,效率要高。但是因为引入了段的概念,所以ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部,因此效率会低于HashMap。不过在多线程情况下,这种性能的牺牲换取数据安全是非常值得的。因此在多线程的情况下应该首选ConcurrentHashMap。(HashMap ConCurrentHashMap 对比

详见:ConCurrentHashMap

SparseArray

private int[] mKeys;//int 类型key数组
private Object[] mValues;//value数组


public SparseArray() {
    this(10);
}

public SparseArray(int initialCapacity) {
    if (initialCapacity == 0) {
        mKeys = EmptyArray.INT;
        mValues = EmptyArray.OBJECT;
    } else {
        mValues = ArrayUtils.newUnpaddedObjectArray(initialCapacity);
        mKeys = new int[mValues.length];
    }
    mSize = 0;
}

SparseArray 如果不设置置顶容量大小,会默认初始一个 容量10 的大小内容。

内部通过 两个数组 来存储数据,一个存储key,一个存储value。key 只支持int类型,避免了装箱

put get 时,都会使用二分查找。所以在数据量大的情况下性能并不明显,将降低至少50%

扩容 当currentSize(当前存储的值的个数)小于等于4的时候,扩容至8,否则扩容至两倍的currentSize

所以用 SparseArray 代替 HashMap 最好是:

ArrayMap

public class ArrayMap<K, V> extends SimpleArrayMap<K, V> implements Map<K, V> {
MapCollections<K, V> mCollections;  


int[] mHashes;
Object[] mArray;

ArrayMap继承自SimpleArrayMap实现了Map接口。ArrayMap内部是两个数组(都为 空数组),一个存放hash值,一个存放键值对 key value。

put
首先就是判段key是否null,是null,hash值直接置为0,如果不为null,通过Obejct的hashCode()方法计算出hash值。然后通过indexfOf方法计算出index的值。indexfOf 内部则是使用二分查找。

扩容

ArrayMap 与 HashMap 对比:

选择使用:

上一篇下一篇

猜你喜欢

热点阅读