Java 集合 散列表hash table
Java 集合 散列表hash table
@author ixenos
摘要:hash table用链表数组实现、解决散列表的冲突:开放地址法 和 链地址法(冲突链表方式)
hash table 是一种数据结构
- hash table 为每个对象计算一个整数,该整数被称为散列码 hash code
- hash code 是由对象的实例域产生的一个整数,具有不同的数据域的对象将产生不同的hash code
- 如果自定义类,就要负责实现这个类的hashCode方法,注意要与equals方法兼容,即如果a.equals(b)为true,则a与b的hash code必须相同
Java中hash table用链表数组实现
- Entry[] table ( HashMap中的key-value都是存储在Entry数组中的 )
- 桶:bucket,用于收集具有相同hash code的元素。要想查找table中对象的位置,就要先计算它的hash code,然后与bucket的总数取余,得到的就是保存这个元素的bucket的index
//图片来自《Core Java》
- 如果bucket中没有其他元素,此时将元素直接插入bucket中就可以了;如果bucket中有元素,需要用新对象与该bucket中所有的对象进行比较,查看这个对象是否已经存在,不存在则修改链表结点索引加入bucket;如果bucket被占满,此现象被称为散列冲突hash collision,此时需要用新对象与该bucket中所有的对象进行比较,查看这个对象是否已经存在
- 散列冲突:hash collision,如果插入到HashTable中的元素太多,就会增加hash collision的可能性,降低性能,所以要指定一个初始的桶数。通常,将桶数设置为预计元素个数的75%~150%;不同key是可以同hash的,然后就加入桶中的双链表,由于链表访问效率低,所以尽量避免hash冲突
-
- 【API文档】
-
public class Hashtable<K,V>extends Dictionary<K,V>implements Map<K,V>, Cloneable, Serializable
此类实现一个哈希表,该哈希表将键映射到相应的值。任何非
null
对象都可以用作键或值。为了成功地在哈希表中存储和获取对象,用作键的对象必须实现hashCode
方法和equals
方法。Hashtable
的实例有两个参数影响其性能:初始容量 和加载因子。容量 是哈希表中桶 的数量,初始容量 就是哈希表创建时的容量。注意,哈希表的状态为 open:在发生“哈希冲突”的情况下,单个桶会存储多个条目,这些条目必须按顺序搜索。加载因子 是对哈希表在其容量自动增加之前可以达到多满的一个尺度。初始容量和加载因子这两个参数只是对该实现的提示。关于何时以及是否调用 rehash 方法的具体细节则依赖于该实现。
- 再散列:rehashed,如果预估过低,HashTable太满,就需要再散列 rehashed 如果装载因子为0.75,而表中超过75%的位置已经填入元素,这个HashTable就会用双倍的桶数再散列rehashed
解决散列表的冲突:开放地址法 和 链地址法(冲突链表方式)
- 根据对冲突的处理方式不同,散列表有两种实现方式:一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。Java HashMap采用的是冲突链表方式。
-
开放地址法:这个方法的基本思想是:当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。
-
-
这个过程可用下式描述: H i ( key ) = ( H ( key )+ d i ) mod m ( i = 1,2,…… , k ( k ≤ m – 1))
-
其中: H ( key ) 为关键字 key 的直接哈希地址, m 为哈希表的长度, di 为每次再探测时的地址增量。
-
采用这种方法时,首先计算出元素的直接哈希地址 H ( key ) ,如果该存储单元已被其他元素占用,则继续查看地址为 H ( key ) + d 2 的存储单元,如此重复直至找到某个存储单元为空时,将关键字为 key 的数据元素存放到该单元。
- 增量 d 可以有不同的取法,并根据其取法有不同的称呼:
- ( 1 ) d i = 1 , 2 , 3 , …… 线性探测再散列;
- ( 2 ) d i = 1^2 ,- 1^2 , 2^2 ,- 2^2 , k^2, -k^2…… 二次探测再散列;
- ( 3 ) d i = 伪随机序列 伪随机再散列;
- 增量 d 可以有不同的取法,并根据其取法有不同的称呼:
-
-
-
-
-
( 1 )线性探测再散列: 32 % 7 = 4 ; 13 % 7 = 6 ; 49 % 7 = 0 ; 55 % 7 = 6 发生冲突,下一个存储地址( 6 + 1 )% 7 = 0 ,仍然发生冲突,再下一个存储地址:( 6 + 2 )% 7 = 1 未发生冲突,可以存入。 22 % 7 = 1 发生冲突,下一个存储地址是:( 1 + 1 )% 7 = 2 未发生冲突; 38 % 7 = 3 ; 21 % 7 = 0 发生冲突,按照上面方法继续探测直至空间 5 ,不发生冲突,所得到的哈希表对应存储位置: 下标: 0 1 2 3 4 5 6 49 55 22 38 32 21 13 ( 2 )二次探测再散列: 下标: 0 1 2 3 4 5 6 49 22 21 38 32 55 13
例子
-
-
-
注意:对于利用开放地址法处理冲突所产生的哈希表中删除一个元素时需要谨慎,不能直接地删除,因为这样将会截断其他具有相同哈希地址的元素的查找地址,所以,通常采用设定一个特殊的标志以示该元素已被删除。
-
- 链地址法(HashMap采用的方法)
- 链地址法解决冲突的做法是:如果散列表空间为 0 ~ m - 1 ,设置一个由 m 个指针分量组成的一维数组 ST[ m ], 凡散列地址为 i 的数据元素都插入到头指针为 ST[ i ] 的链表中。这种方法有点近似于邻接表的基本思想,且这种方法适合于冲突比较严重的情况
hash table可以用于实现几个重要的数据结构
HashSet内部维护了一个HashMap<E,Object>对象用以存储set对象,屏蔽了map中的键,大部分方法的实现都借助了HashMap的方法。
-
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; }
- 从源码中可以看出:contains方法能用来快速查看是否某个元素已经出现在集中,因为它只在某个桶中查找元素,而不必查看集合中的所有元素
- HashSet迭代器将依次访问所有的bucket,由于hash将元素分散在表的各个位置上(只有不关心集合中元素的顺序时才应该使用HashSet),所以访问他们的顺序几乎是随机的(当然这随机是"固定"了的)
由于元素的hash code决定所在bucket,因此修改集set中元素的内部特征(实例域)的时候,要小心hash code码改变导致元素在数据结构中的位置的变化