Refer from http://blog.csdn.net/zhangweikai966/article/details/8266883
哈希表(Hash Table,又叫散列表),是存储键值对(Key-value)的表,之所以不叫它Map(键值对一起存储一般叫做Map),是因为它下面的特性:它能把关键码(key)映射到表中的一个位置来直接访问,这样访问速度就非常快。其中的映射函数称为散列函数(Hash function)。
1) 对于关键字key, f(key)是其存储位置,f则是散列函数
2) 如果key1 != key2 但是 f(key1) == f(key2),这种现象称为冲突(collison)。冲突不可避免,这是因为key值无限而表容量总是有限(*见篇末思考题*)。我们追求的是对任意关键字,散列到表中的地址概率是相等的,这样的散列函数为均匀散列函数。
散列函数有多种
× 直接定址法:取关键字或关键字的某个线性函数值为散列地址。即H(key)=key或H(key) = a·key + b,其中a和b为常数(这种散列函数叫做自身函数)
× 数字分析法
× 平方取中法
× 折叠法
× 随机数法
× 除留余数法:取关键字被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。对p的选择很重要,一般取素数或m,若p选的不好,容易产生同义词。
可以想像,当表中的数据个数接近表的容量大小时,发生冲突的概率会明显增大,因此,在“数据个数/表容量”到达某个比例的时侯,需要扩大表的容量,这个比例称为“装填因子”(load factor).
解决冲突主要有下面两类方法:
× 分离链接法,就是对hash到同一地址的不同元素,用链表连起来,也叫拉链法
× 开放定址法,如果地址有冲突,就在此地址附近找。包括线性探测法,平方探测法,双散列等
然后来看一下Java的Hashtable实现
java.util.Hashtable的本质是个数组,数组的元素是linked的键值对(单向链表)。
- private transient Entry[] table; // Entry数组
- private static class Entry<K,V> implements Map.Entry<K,V> {
- int hash;
- K key;
- V value;
- Entry<K,V> next; // Entry此处表明是个单链表
- ...
- }
我们可以使用指定数组大小、装填因子的构造函数,也可以使用默认构造函数,默认数组的大小是11,装填因子是0.75.
- public Hashtable(int initialCapacity, float loadFactor) {
- ...
- }
- public Hashtable() {
- this(11, 0.75f);
- }
当要扩大数组时,大小变为oldCapacity * 2 + 1,当然这无法保证数组的大小总是素数。
来看下其中的元素插入的方法,put方法:
- public synchronized V put(K key, V value) {
- // Make sure the value is not null
- if (value == null) {
- throw new NullPointerException();
- }
-
- // Makes sure the key is not already in the hashtable.
- Entry tab[] = table;
- int hash = key.hashCode();
- int index = (hash & 0x7FFFFFFF) % tab.length;
- for (Entry<K, V> e = tab[index]; e != null; e = e.next) {
- if ((e.hash == hash) && e.key.equals(key)) {
- V old = e.value;
- e.value = value;
- return old;
- }
- }
- }
Java中Object类有几个方法,其中一个是hashCode(), 这说明Java中所有对象都具有这一方法,调用可以得到对象自身的hash码。对表的长度取余得址,并在冲突位置使用链表。
HashMap与Hashtable的功能几乎一样。但HashMap的的初始数组大小是16而不是11,当要扩大数组时,大小变为原来的2倍,默认的装填因子也是0.75. 其put方法如下,对hash值和index都有更改:
- public V put(K key, V value) {
- if (key == null)
- return putForNullKey(value);
- int hash = hash(key.hashCode());
- int i = indexFor(hash, table.length);
- for (Entry<K, V> e = table[i]; e != null; e = e.next) {
- Object k;
- if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
- V oldValue = e.value;
- e.value = value;
- e.recordAccess(this);
- return oldValue;
- }
- }
-
- modCount++;
- addEntry(hash, key, value, i);
- return null;
- }
-
-
- /**
- * Applies a supplemental hash function to a given hashCode, which
- * defends against poor quality hash functions. This is critical
- * because HashMap uses power-of-two length hash tables, that
- * otherwise encounter collisions for hashCodes that do not differ
- * in lower bits. Note: Null keys always map to hash 0, thus index 0.
- */
- static int hash(int h) {
- // This function ensures that hashCodes that differ only by
- // constant multiples at each bit position have a bounded
- // number of collisions (approximately 8 at default load factor).
- h ^= (h >>> 20) ^ (h >>> 12);
- return h ^ (h >>> 7) ^ (h >>> 4);
- }
-
- /**
- * Returns index for hash code h.
- */
- static int indexFor(int h, int length) {
- return h & (length-1);
- }
再看看其它开源的Java库中的Hashtable
目前存在多个开源的Java Collection实现,各个目的不同,侧重点也不同。以下对开源框架中哈希表的分析主要从几个方面入手:默认装填因子和capacity扩展方式,散列函数以及解决冲突的方法。
1. Trove - Trove库提供一套高效的基础集合类。
gnu.trove.set.hash.THashMap的继承关系:THashMap -> TObjectHash -> THash,其内部的键和值使分别用2个数组表示。其解决冲突的方式采用开放寻址法,开放寻址法对空间要求较高,因此其默认装填因子load factor是0.5,而不是0.75. 下面看代码一步步解释:
默认初始化,装填因子0.5,数组大小始从素数中取,也就是始终是素数。
- /** the load above which rehashing occurs. */
- public static final float DEFAULT_LOAD_FACTOR = 0.5f;
-
- protected int setUp( int initialCapacity ) {
- int capacity;
- capacity = PrimeFinder.nextPrime( initialCapacity );
- computeMaxSize( capacity );
- computeNextAutoCompactionAmount( initialCapacity );
- return capacity;
- }
然后看其put方法,insertKey(T key)是其散列算法,hash码对数组长度取余后,得到index,首先检查该位置是否被占用,如果被占用,使用双散列算法解决冲突,也就是代码中的insertKeyRehash()方法。
- public V put(K key, V value) {
- // insertKey() inserts the key if a slot if found and returns the index
- int index = insertKey(key);
- return doPut(value, index);
- }
-
-
- protected int insertKey(T key) {
- consumeFreeSlot = false;
-
- if (key == null)
- return insertKeyForNull();
-
- final int hash = hash(key) & 0x7fffffff;
- int index = hash % _set.length;
- Object cur = _set[index];
-
- if (cur == FREE) {
- consumeFreeSlot = true;
- _set[index] = key; // insert value
- return index; // empty, all done
- }
-
- if (cur == key || equals(key, cur)) {
- return -index - 1; // already stored
- }
-
- return insertKeyRehash(key, index, hash, cur);
- }
2. Javolution - 对实时、内置、高性能系统提供Java解决方案
Javolution中的哈希表是jvolution.util.FastMap, 其内部是双向链表,默认初始大小是16,扩展时变为2倍。并没有显式定义load factor, 从下面语句可以知道,其值为0.5
- if (map._entryCount + map._nullCount > (entries.length >> 1)) { // Table more than half empty.
- map.resizeTable(_isShared);
- }
再看下put函数,比较惊人的是其index和slot的取得,完全是用hashkey移位的方式取得的,这样同时计算了index和避免了碰撞。
- private final Object put(Object key, Object value, int keyHash,
- boolean concurrent, boolean noReplace, boolean returnEntry) {
- final FastMap map = getSubMap(keyHash);
- final Entry[] entries = map._entries; // Atomic.
- final int mask = entries.length - 1;
- int slot = -1;
- for (int i = keyHash >> map._keyShift;; i++) {
- Entry entry = entries[i & mask];
- if (entry == null) {
- slot = slot < 0 ? i & mask : slot;
- break;
- } else if (entry == Entry.NULL) {
- slot = slot < 0 ? i & mask : slot;
- } else if ((key == entry._key) || ((keyHash == entry._keyHash) && (_isDirectKeyComparator ? key.equals(entry._key)
- : _keyComparator.areEqual(key, entry._key)))) {
- if (noReplace) {
- return returnEntry ? entry : entry._value;
- }
- Object prevValue = entry._value;
- entry._value = value;
- return returnEntry ? entry : prevValue;
- }
- }
- ...
- }
Refer from http://blog.csdn.net/jackydai987/article/details/6673063
线性哈希-line hash
线性哈希是一种动态扩展哈希表的方法。
线性哈希的数学原理:
假定key = 5 、 9 、13
key % 4 = 1
现在我们对8求余
5 % 8 = 5
9 % 8=1
13 % 8 = 5
由上面的规律可以得出
(任意key) % n = M
(任意key) %2n = M或 (任意key) %2n = M + n
线性哈希的具体实现:
我们假设初始化的哈希表如下:
分裂点 |
桶编号 |
桶中已存储的Key |
溢出key |
* |
0 |
4,8,12 |
|
|
1 |
5,9 |
|
|
2 |
6 |
|
|
3 |
7,11,15,19, 23 |
|
Figure1
为了方便叙述,我们作出以下假定:
1:为了使哈希表能进行动态的分裂,我们从桶0开始设定一个分裂点。
2:一个桶的容量为listSize = 5,当桶的容量超出后就从分裂点开始进行分裂。
3:hash函数为 h0 = key %4 h1 = key % 8,h1会在分裂时使用。
4:整个表初始化包含了4个桶,桶号为0-3,并已提前插入了部分的数据。
分裂过程如下:
现在插入key = 27
1:进行哈希运算,h0 = 27 % 4 = 3
2:将key = 27插入桶3,但发现桶3已经达到了桶的容量,所以触发哈希分裂
3:由于现在分裂点处于0桶,所以我们对0桶进行分割。这里需要注意虽然这里是3桶满了,但我们并不会直接从3桶进行分割,而是从分割点进行分割。这里为什么这么做,在下面会进一步介绍。
4:对分割点所指向的桶(桶0)所包含的key采用新的hash函数(h1)进行分割。
对所有key进行新哈希函数运算后,将产生如下的哈希表
分裂点 |
桶编号 |
桶中已存储的Key |
溢出key |
|
0 |
8 |
|
* |
1 |
5,9 |
|
|
2 |
6 |
|
|
3 |
7,11,15,19, 23 |
27 |
|
4 |
4,12 |
|
Figure2
5:虽然进行了分裂,但桶3并不是分裂点,所以桶3会将多出的key,放于溢出页.,一直等到桶3进行分裂。
6:进行分裂后,将分裂点向后移动一位。
一次完整的分裂结束。
key的读取:
采用h0对key进行计算。
如果算出的桶号小于了分裂点,表示桶已经进行的分裂,我们采用h1进行hash运算,算出key所对应的真正的桶号。再从真正的桶里取出value
如果算出的桶号大于了分裂点,那么表示此桶还没进行分裂,直接从当前桶进行读取value。
说明:
1:如果下一次key插入0、1、2、4桶,是不会触发分裂。(没有超出桶的容量)如果是插入桶3,用户在实现时可以自己设定,可以一旦插入就触发,也可以等溢出页达到listSize再触发新的分裂。
2:现在0桶被分裂了,新数据的插入怎么才能保证没分裂的桶能正常工作,已经分裂的桶能将部分插入到新分裂的桶呢?
只要分裂点小于桶的总数,我们依然采用h0函数进行哈希计算。
如果哈希结果小于分裂号,那么表示这个key所插入的桶已经进行了分割,那么我就采用h1再次进行哈希,而h1的哈希结果就这个key所该插入的桶号。
如果哈希结果大于分裂号,那么表示这个key所插入的桶还没有进行分裂。直接插入。
这也是为什么虽然是桶3的容量不足,但分裂的桶是分裂点所指向的桶。如果直接在桶3进行分裂,那么当新的key插入的时候就不能正常的判断哪些桶已经进行了分裂。
3:如果使用分割点,就具备了无限扩展的能力。当分割点移动到最后一个桶(桶3)。再出现分裂。那么分割点就会回到桶0,到这个时候,h0作废,h1替代h0, h2(key % 12)替代h1。那么又可以开始动态分割。那个整个初始化状态就发生了变化。就好像没有发生过分裂。那么上面的规则就可以循环使用。
3:线性哈希的论文中是按上面的规则来进行分裂的。其实我们可以安装自己的实际情况来进行改动。
假如我们现在希望去掉分割点,一旦哪个桶满了,马上对这个桶进行分割。
可以考虑了以下方案:
1:为所有桶增加一个标志位。初始化的时候对所有桶的标志位清空。
2:一旦某个桶满了,直接对这个桶进行分割,然后将设置标志位。当新的数据插入的时候,经过哈希计算(h0)发现这个桶已经分裂了,那么就采用新的哈希函数(h1)来计算分裂之后的桶号。在读取数据的时候处理类似。
Linehash 实现代码如下:
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
-
- public class LineHash {
-
- public int pageSize;
-
- public int overPoint = 0;
-
- public int listSize = 4;
-
- public int initlistSize = 4;
-
- public int workRound = 1;
-
- public List<Map<Integer, String>> hash = null;
-
- public LineHash(int pageSIze) {
- this.pageSize = pageSIze;
- hash = new ArrayList<Map<Integer, String>>(4);
- for (int i = 0; i < listSize; i++) {
- hash.add(new HashMap<Integer, String>());
- }
- }
-
- public String getKeyValue(int key){
- int index = hashFun(key, workRound);
- if(index < overPoint){
- index = hashFun(key, workRound + 1);
- }
- return hash.get(index).get(key);
- }
-
- public void addKeyValue(int key, String value) {
- int index = hashFun(key, workRound);
- if(index < overPoint){
- index = hashFun(key, workRound + 1);
- }
- Map<Integer, String> map = hash.get(index);
- if (map.size() < pageSize) {
- map.put(key, value);
- } else {
- map.put(key, value);
- splitHash();
- }
- }
-
- public int hashFun(int key, int f1) {
- return key % (4 * f1);
- }
-
- public void splitHash() {
- Map<Integer, String> OldMap = hash.get(overPoint);
- Map<Integer, String> NewMap = new HashMap<Integer, String>();
-
- Integer[] keyList = OldMap.keySet().toArray(new Integer[0]);
- for (int i = 0; i < keyList.length; i++) {
- int key = keyList[i].intValue();
- int index = hashFun(key, workRound + 1);
- if (index >= listSize) {
- String value = OldMap.get(key);
- OldMap.remove(key);
- NewMap.put(key, value);
- }
- }
- hash.add(NewMap);
- listSize++;
- overPoint++;
- if(overPoint >= initlistSize){
- workRound++;
- initlistSize = initlistSize * 2;
- overPoint = 0;
- }
- }
- }
测试代码:
- public class testLineHash {
- public static void main(String args[]){
- LineHash hash = new LineHash(3);
- hash.addKeyValue(4, "this");
- hash.addKeyValue(8, "is");
- hash.addKeyValue(12, "a");
- hash.addKeyValue(16, "test");
- hash.addKeyValue(20, "!!!!");
- hash.addKeyValue(24, "~~~");
- hash.addKeyValue(28, "dsd");
- hash.addKeyValue(32, "gg22");
-
- for(int i = 4; i <= 24;)
- {
- System.out.println(hash.getKeyValue(i));
- i = i + 4;
- }
- }
- }