散列表

常用的解决散列冲突的方法

开放寻址法

开放寻址法不像链表法,需要拉很多链表。散列表中的数据都存储在数组中,可以有效地利用 CPU 缓存加快查询速度。而且,这种方法实现的散列表,序列化起来比较简单。链表法包含指针,序列化起来就没那么容易。

所以当数据量小,装载因子小的适合适合用开放寻址法去解决冲突。例如ThreadLocalMap就是使用的开放寻址法。

 1         private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
 2             Entry[] tab = table;
 3             int len = tab.length;
 4
 5             while (e != null) {
 6                 ThreadLocal<?> k = e.get();
 7                 if (k == key)
 8                     return e;
 9                 if (k == null)
10                     expungeStaleEntry(i);
11                 else
12                     i = nextIndex(i, len);
13                 e = tab[i];
14             }
15             return null;
16         }

 

链表法

链表法内存利用率高,链表的节点可以在需要时再申请,不需要像开放寻址法事先就申请。但使用的是链表,所以不好利用CPU缓存加速。

链表法比起开放寻址法,对大装载因子的容忍度更高。开放寻址法只能适用装载因子小于 1 的情况。接近 1 时,就可能会有大量的散列冲突,导致大量的探测、再散列等,性能会下降很多。但是对于链表法来说,只要散列函数的值随机均匀,即便装载因子变成 10,也就是链表的长度变长了而已,虽然查找效率有所下降,但是比起顺序查找还是快很多。

将链表法中的链表改造为其他高效的动态数据结构,例如跳表、红黑树,可以使得散列表更加高效。

 

Java中的HashMap

1)初始大小

初始大小为16,如果提前知道数据量可以在创建时给定一个值,减少动态扩容次数,提高性能。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

2)装载因子

默认负载因子为0.75,超过时会扩容为原来的两倍。

    static final float DEFAULT_LOAD_FACTOR = 0.75f;
...
...
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }

3)解决散列冲突的方法

HashMap底层使用链表法解决散列冲突,但是链表过长时还是会严重影响HashMap的性能。所以在JDK1.8中,当链表长度大于8时,会将链表变为红黑树,当链表小于6时,又将红黑树变为链表。

    static final int TREEIFY_THRESHOLD = 8;

    static final int UNTREEIFY_THRESHOLD = 6;

4)散列函数

 1    static final int hash(Object key) {
 2         int h;
 3         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
 4     }
 5 ...
 6 ...
 7     public V put(K key, V value) {
 8         return putVal(hash(key), key, value, false, true);
 9     }
10     final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
11                    boolean evict) {
12         Node<K,V>[] tab; Node<K,V> p; int n, i;
13         if ((tab = table) == null || (n = tab.length) == 0)
14             n = (tab = resize()).length;
15         if ((p = tab[i = (n - 1) & hash]) == null)
16             tab[i] = newNode(hash, key, value, null);
17 ...

其中hash()方法被称为"扰动函数",它混合原始哈希码的高位和低位,以此来加大低位的随机性。在执行put()方法时,还将散列值和数组长度做了"与"操作,这也解释了HashMap的数组长度为什么要为2的整数次幂(例如数组长度为16时,n - 1 = 15,二进制代码为0000 0000 0000 1111,这样就可以形成一个"低位掩码",和散列值做"与"操作,保证数组索引最大不会超过15)。

扰动函数详细解释链接https://www.zhihu.com/question/20733617

 

posted @ 2020-11-14 21:44  kpmv  阅读(174)  评论(0)    收藏  举报