Java 散列集笔记
散列表
散列表(hash table)为每个对象计算一个整数,称为散列码(hash code)。 若需要自定义类,就要负责实现这个类的hashCode方法。注意自己实现的hashCode方法应该与equals方法兼容,即如果a.equals(b)为true,a与b必须具有相同的散列码。
hashCode方法
散列码是由对象导出的一个整型值,散列码是没有规律的,即若x与y是两个不同的对象,二者的散列码基本不会相同。 String类用下列算法计算散列码:
int hash = 0;
for (int i = 0; i < length(); i++)
hash = 31 * hash + charAt(i);
由于hashCode方法定义在Object类中,因此每个对象都有一个默认的散列码,其值为对象的存储地址。 hashCode方法应该返回一个整型数值(也可以是负数),并合理地组合实例域的散列码,一边能够让各个不同的对象产生的散列码更均匀。 需要组合多个散列值时,可以调用Objects.hash并提供多个参数。这个方法会对各个参数调用Objects.hashCode,并组合这些散列值。
static int hash(Object... Objects)
返回一个散列码,由提供的所有对象的散列码组合而得到。
散列码应该能够快速地计算出来,并且这个计算只与要散列的对象状态有关,与散列表中其它对象无关。
Java中的散列表实现
Java中散列表用链表数组实现,每个列表被称为桶(bucket)。要想查找table中对象的位置,就要先计算它的散列码,然后与桶的总数取余,得到的就是保存这个元素的桶的索引。 如果bucket中没有其他元素,此时将元素直接插入bucket中就可以了;如果bucket中有元素,需要用新对象与该bucket中所有的对象进行比较,查看这个对象是否已经存在,不存在则修改链表结点索引加入bucket;如果bucket被占满,此现象被称为散列冲突(hash collision),此时需要用新对象与该bucket中所有的对象进行比较,查看这个对象是否已经存在。
桶数设置
若想更多地控制散列表的运行性能,就要指定一个初始的桶数。桶数是指用于收集具有相同散列值的桶的数目。 如果大致知道最终会有多少个元素要插入到散列表中,就可以设置桶数。通常将桶数设置为预计元素个数的75% ~ 150%。 有些研究人员认为,最好将桶数设置为一个素数,以防键的集聚。
设有哈希H(c) = c % N 取N为合数N = 2 ^ 3 = 8。 H(11100)= H(36)= 4; H(10100)= H(28)= 4; c的二进制第四位不参与运算,即无论取何值都不影响计算结果。 这样H(c)无法完整地反映c的特性,增大导致冲突的几率。
此外,实际中往往关键字有某种规律,例如大量的等差数列,那么公差和模数不互质的时候发生碰撞的概率会变大,而用质数可以在很大程度上回避这个问题,基本可以保证c的每一位都参与c的运算,从而在常见应用中减少冲突。
若散列表太慢,就需要再散列(rehashed)。需要创建一个桶数更多的表,并将所有元素插入到这个新表中,然后丢弃原来的表。装填因子决定何时对散列表进行再散列。一般0.75比较合理,即表中超过75%的位置已经填入元素时,这个表就会用双倍的桶数自动地进行再散列。
HashMap在根据用户传入的capacity计算得到默认容量,并不考虑load factor的因素,而是直接计算出第一个大于这个数字的2的幂。
设置默认容量可以参考JDK8中putAll的实现,即若明确知道HashMap中元素的个数,计算expectedSize / 0.75F + 1.0F是一个在性能上相对比较好的选择,但同时也会牺牲部分内存。
HashMap中使用HashMap(int initialCapacity)来实现。
参考:https://mp.weixin.qq.com/s/SFss68LcQc5ZFGpu-Ssgog
散列冲突的解决方法
-
开放地址法 当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。 公式:Hi=(H(key)+di) MOD m i=1,2,…,k (k <= m 1), H(key)为key的直接哈希地址,m为哈希表的长度,di为每次再探测时的地址增量。 增量di可以有不同的取法,并根据其取法有不同的称呼: ( 1 ) d i = 1 , 2 , 3 , …… 线性探测再散列; ( 2 ) d i = 1^2 ,- 1^2 , 2^2 ,- 2^2 , k^2, -k^2…… 二次探测再散列; ( 3 ) d i = 伪随机序列 伪随机再散列; 注意:对于利用开放地址法处理冲突所产生的哈希表中删除一个元素时需要谨慎,不能直接地删除,因为这样将会截断其他具有相同哈希地址的元素的查找地址,所以,通常采用设定一个特殊的标志以示该元素已被删除。
-
链地址法 如果散列表空间为 0 ~ m – 1 ,设置一个由 m 个指针分量组成的一维数组 ST[ m ], 凡散列地址为 i 的数据元素都插入到头指针为 ST[ i ] 的链表中。这种方法有点近似于邻接表的基本思想,且这种方法适合于冲突比较严重的情况。
-
再哈希法 当发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突。 缺点:计算时间增加。
HashTable实现的数据结构
散列表可以用于实现几个重要的数据结构,其中最简单的是set类型。 Java集合类库中提供了一个HashSet类。散列集迭代器将依次访问所有的桶,由于散列将各个元素分散在表的各个位置上,所以访问它们的顺序几乎是随机的。
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable{
private transient HashMap<E,Object> map;
...
public HashSet() { map = new HashMap<>(); }
...
}
contains方法已经被重新定义,用于快速地查看是否某个元素已经出现在集中。它只需在某个桶中查找元素。
//HashSet的contains方法源码(借助HashMap的方法)
public boolean contains(Object o) {
return map.containsKey(o);
}
//来自HashMap的源码
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
public boolean containsKey(Object key) { //被HashSet的contains方法调用
return getNode(hash(key), key) != null;
}
参考: 《Core Java》