散列表
常用的解决散列冲突的方法
开放寻址法
开放寻址法不像链表法,需要拉很多链表。散列表中的数据都存储在数组中,可以有效地利用 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

浙公网安备 33010602011771号