闭散列法,也叫开放定址法,发生哈希冲突时,如果哈希表未被装满,则把key存放到冲突位置的“下一个”空位置中去。
线性探测找“下一个”空位置:从发生冲突的位置开始,依次向后探测“下一个”空位置。
线性探测的缺点:
①把更多冲突的元素聚集在一起,这与其找下一个空位置有关
②不能随便删除哈希表1中已有的元素,如删除5,则55查找起来可能会受影响
为了避免把更多冲突的元素聚集在一起,二次探测找“下一个”空位置的方法为:Hi = (H0 + i ^2) % capacity 或 Hi = (H0 - i ^2) % capacity
与线性探测相比二次探测中冲突的元素较分开。
闭散列的缺点:空间利用率较低,也是哈希的缺陷。
开散列法,也叫链地址法/开链法,使用哈希桶方式解决哈希冲突。哈希桶方式解决哈希冲突:哈希表是数组+链表的结构,当链表的长度超过8 && 数组的长度超过64时,链表变成红黑树。向哈希表中插入元素时JDK1.8之前是头插法,JDK1.8之后是尾插法。
这里用了开散列法解决哈希冲突。
//key-value模型
class Person {
public String id;
public Person(String id) {
this.id = id;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return Objects.equals(id, person.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
public class HashBucket<K, V> {
private static class Node<K, V> {
private K key;
private V val;
Node next;
public Node(K key, V value) {
this.key = key;
this.val = value;
}
}
private Node<K, V>[] array;
private int usedSize;
private static final double LOAD_FACTOR = 0.75;
private static final int DEFAULT_SIZE = 8;//默认桶的大小
public HashBucket() {
array = (Node<K, V>[])new Node[10];
}
public void put(K key, V val) {
int hash = key.hashCode();
int index = hash % array.length;
Node<K, V> cur = array[index];
while(cur != null) {
if(cur.key.equals(key)) {
cur.val = val;
return;
}
cur = cur.next;
}
Node<K, V> node = new Node<>(key, val);
//头插法
node.next = array[index];
array[index] = node;
usedSize++;
if(loadFactor() >= LOAD_FACTOR) {
resize();
}
}
//扩容要把所有的元素重新进行哈希
private void resize() {
Node<K, V>[] tmpArr = new Node[array.length * 2];
//遍历原来数组,将原来数组的元素重新哈希到新的数组当中。因为要遍历原来的数组,所以扩容时要申请一个新的数组
for (int i = 0; i < array.length; i++) {
Node<K, V> cur = array[i];
while(cur != null) {
Node<K, V> curNext = cur.next;
int hash = cur.key.hashCode();
int newIndex = hash % array.length;
//头插法
cur.next = tmpArr[newIndex];
tmpArr[newIndex] = cur;
cur = curNext;
}
}
array = tmpArr;
}
private double loadFactor() {
return usedSize * 1.0 / array.length;//散列表的载荷因子=填入表中的元素个数/散列表的长度
}
public V get(K key) {
int hash = key.hashCode();
int index = hash % array.length;
Node<K, V> cur = array[index];
while (cur != null) {
if (cur.key.equals(key)) {
return cur.val;
}
}
return null;
}
}
HashMap的其中一个构造方法HashMap(int initialCapacity, float loadFactor),这个构造方法里面有一个tableSizeFor(int cap)方法,tableSizeFor(int cap)方法的作用是返回一个接近目标容量的二次幂,如HashMap(int initialCapacity, float loadFactor)中的initialCapacity给了1000,则tableSizeFor(int cap)返回1024(返回大于1000的不返回小于1000的)。所以实例化HashMap时,initialCapacity给了1000,最后数组容量则是1024。
(h = key.hashCode()) ^ (h >>> 16)的目的是使关键字在哈希表中尽可能更均匀地分布。putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)中有(n - 1) & hash,n是数组长度,当(n - 1)的值比较小时,(n - 1)和hash二进制序列参与到计算中的只有低位。多个hash和(n - 1)进行计算,如果hash和(n - 1)这两者的值的二进制序列均是低位相同,高位不同的话,(n - 1) & hash计算出来的数组下标都是同一个,增加了冲突的几率,所以要用(h = key.hashCode()) ^ (h >>> 16)计算hash。当n是二次幂时,hash%n和hash&(n-1)的结果一样。&的结果使二进制序列更向0集中,|的结果使二进制序列更向1集中, ^的结果使二进制序列更加倾向保留参与计算的两者的二进制序列各自的特征。(h = key.hashCode()) ^ (h >>> 16)使hash的低位二进制序列既保留hashCode()二进制序列高位的特征,又保留了hashCode()二进制序列低位的特征。