java中哈希表用途

来源:互联网 发布:bbc news软件下载 编辑:程序博客网 时间:2024/09/21 06:09

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的键值对(单向链表)。 

Java代码  收藏代码
  1. private transient Entry[] table; // Entry数组  


Java代码  收藏代码
  1. private static class Entry<K,V> implements Map.Entry<K,V> {  
  2.     int hash;  
  3.     K key;  
  4.     V value;  
  5.     Entry<K,V> next; // Entry此处表明是个单链表  
  6.     ...  
  7. }  


我们可以使用指定数组大小、装填因子的构造函数,也可以使用默认构造函数,默认数组的大小是11,装填因子是0.75. 
Java代码  收藏代码
  1. public Hashtable(int initialCapacity, float loadFactor) {  
  2. ...  
  3. }  
  4. public Hashtable() {  
  5.     this(11, 0.75f);  
  6. }  


当要扩大数组时,大小变为oldCapacity * 2 + 1,当然这无法保证数组的大小总是素数。 
来看下其中的元素插入的方法,put方法: 
Java代码  收藏代码
  1. public synchronized V put(K key, V value) {  
  2.     // Make sure the value is not null  
  3.     if (value == null) {  
  4.         throw new NullPointerException();  
  5.     }  
  6.       
  7.     // Makes sure the key is not already in the hashtable.  
  8.     Entry tab[] = table;  
  9.     int hash = key.hashCode();  
  10.     int index = (hash & 0x7FFFFFFF) % tab.length;  
  11.     for (Entry<K, V> e = tab[index]; e != null; e = e.next) {  
  12.         if ((e.hash == hash) && e.key.equals(key)) {  
  13.             V old = e.value;  
  14.             e.value = value;  
  15.             return old;  
  16.         }  
  17.     }  
  18. }  


Java中Object类有几个方法,其中一个是hashCode(), 这说明Java中所有对象都具有这一方法,调用可以得到对象自身的hash码。对表的长度取余得址,并在冲突位置使用链表。 

HashMap与Hashtable的功能几乎一样。但HashMap的的初始数组大小是16而不是11,当要扩大数组时,大小变为原来的2倍,默认的装填因子也是0.75. 其put方法如下,对hash值和index都有更改: 
Java代码  收藏代码
  1. public V put(K key, V value) {  
  2.     if (key == null)  
  3.         return putForNullKey(value);  
  4.     int hash = hash(key.hashCode());  
  5.     int i = indexFor(hash, table.length);  
  6.     for (Entry<K, V> e = table[i]; e != null; e = e.next) {  
  7.         Object k;  
  8.         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.             V oldValue = e.value;  
  10.             e.value = value;  
  11.             e.recordAccess(this);  
  12.             return oldValue;  
  13.         }  
  14.     }  
  15.   
  16.     modCount++;  
  17.     addEntry(hash, key, value, i);  
  18.     return null;  
  19. }  
  20.   
  21.   
  22. /** 
  23.  * Applies a supplemental hash function to a given hashCode, which 
  24.  * defends against poor quality hash functions.  This is critical 
  25.  * because HashMap uses power-of-two length hash tables, that 
  26.  * otherwise encounter collisions for hashCodes that do not differ 
  27.  * in lower bits. Note: Null keys always map to hash 0, thus index 0. 
  28.  */  
  29. static int hash(int h) {  
  30.     // This function ensures that hashCodes that differ only by  
  31.     // constant multiples at each bit position have a bounded  
  32.     // number of collisions (approximately 8 at default load factor).  
  33.     h ^= (h >>> 20) ^ (h >>> 12);  
  34.     return h ^ (h >>> 7) ^ (h >>> 4);  
  35. }  
  36.   
  37. /** 
  38.  * Returns index for hash code h. 
  39.  */  
  40. static int indexFor(int h, int length) {  
  41.     return h & (length-1);  
  42. }  



再看看其它开源的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,数组大小始从素数中取,也就是始终是素数。 
Java代码  收藏代码
  1. /** the load above which rehashing occurs. */  
  2. public static final float DEFAULT_LOAD_FACTOR = 0.5f;  
  3.   
  4. protected int setUp( int initialCapacity ) {  
  5.     int capacity;  
  6.     capacity = PrimeFinder.nextPrime( initialCapacity );  
  7.     computeMaxSize( capacity );  
  8.     computeNextAutoCompactionAmount( initialCapacity );  
  9.     return capacity;  
  10. }  


然后看其put方法,insertKey(T key)是其散列算法,hash码对数组长度取余后,得到index,首先检查该位置是否被占用,如果被占用,使用双散列算法解决冲突,也就是代码中的insertKeyRehash()方法。 
Java代码  收藏代码
  1. public V put(K key, V value) {  
  2.     // insertKey() inserts the key if a slot if found and returns the index  
  3.     int index = insertKey(key);  
  4.     return doPut(value, index);  
  5. }  
  6.   
  7.   
  8. protected int insertKey(T key) {  
  9.     consumeFreeSlot = false;  
  10.   
  11.     if (key == null)  
  12.         return insertKeyForNull();  
  13.   
  14.     final int hash = hash(key) & 0x7fffffff;  
  15.     int index = hash % _set.length;  
  16.     Object cur = _set[index];  
  17.   
  18.     if (cur == FREE) {  
  19.         consumeFreeSlot = true;  
  20.         _set[index] = key;  // insert value  
  21.         return index;       // empty, all done  
  22.     }  
  23.   
  24.     if (cur == key || equals(key, cur)) {  
  25.         return -index - 1;   // already stored  
  26.     }  
  27.   
  28.     return insertKeyRehash(key, index, hash, cur);  
  29. }  




2. Javolution - 对实时、内置、高性能系统提供Java解决方案 

Javolution中的哈希表是jvolution.util.FastMap, 其内部是双向链表,默认初始大小是16,扩展时变为2倍。并没有显式定义load factor, 从下面语句可以知道,其值为0.5 
Java代码  收藏代码
  1. if (map._entryCount + map._nullCount > (entries.length >> 1)) { // Table more than half empty.  
  2.     map.resizeTable(_isShared);  
  3. }  


再看下put函数,比较惊人的是其index和slot的取得,完全是用hashkey移位的方式取得的,这样同时计算了index和避免了碰撞。 
Java代码  收藏代码
  1. private final Object put(Object key, Object value, int keyHash,  
  2.         boolean concurrent, boolean noReplace, boolean returnEntry) {  
  3.     final FastMap map = getSubMap(keyHash);  
  4.     final Entry[] entries = map._entries; // Atomic.  
  5.     final int mask = entries.length - 1;  
  6.     int slot = -1;  
  7.     for (int i = keyHash >> map._keyShift;; i++) {  
  8.         Entry entry = entries[i & mask];  
  9.         if (entry == null) {  
  10.             slot = slot < 0 ? i & mask : slot;  
  11.             break;  
  12.         } else if (entry == Entry.NULL) {  
  13.             slot = slot < 0 ? i & mask : slot;  
  14.         } else if ((key == entry._key) || ((keyHash == entry._keyHash) && (_isDirectKeyComparator ? key.equals(entry._key)  
  15.                 : _keyComparator.areEqual(key, entry._key)))) {  
  16.             if (noReplace) {  
  17.                 return returnEntry ? entry : entry._value;  
  18.             }  
  19.             Object prevValue = entry._value;  
  20.             entry._value = value;  
  21.             return returnEntry ? entry : prevValue;  
  22.         }  
  23.     }  
  24.     ...  
  25. }      


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 实现代码如下:

 

[java] view plaincopy
  1. import java.util.ArrayList;  
  2. import java.util.HashMap;  
  3. import java.util.List;  
  4. import java.util.Map;  
  5.   
  6. public class LineHash {  
  7.       
  8.     public int pageSize;  //桶的容量  
  9.   
  10.     public int overPoint = 0//分裂点  
  11.   
  12.     public int listSize = 4//哈希表的初始大小  
  13.   
  14.     public int initlistSize = 4//哈希大小的记录值  
  15.        
  16.     public int workRound = 1;  //分裂轮数  
  17.       
  18.     public List<Map<Integer, String>> hash = null//模拟哈希表  
  19.   
  20.     public LineHash(int pageSIze) {  
  21.         this.pageSize = pageSIze;  
  22.         hash = new ArrayList<Map<Integer, String>>(4);  
  23.         for (int i = 0; i < listSize; i++) {  
  24.             hash.add(new HashMap<Integer, String>()); //向哈希表中初始化桶  
  25.         }  
  26.     }  
  27.     //查询函数  
  28.     public String getKeyValue(int key){  
  29.         int index = hashFun(key, workRound); //根据分裂轮数调用不同的哈希函数  
  30.         if(index < overPoint){               //当前桶产生了分裂  
  31.             index = hashFun(key, workRound + 1); //采用新的哈希函数进行计算  
  32.         }  
  33.         return hash.get(index).get(key);  
  34.     }  
  35.     //添加函数  
  36.     public void addKeyValue(int key, String value) {   
  37.         int index = hashFun(key, workRound);     
  38.         if(index < overPoint){  
  39.             index = hashFun(key, workRound + 1);  
  40.         }  
  41.         Map<Integer, String> map = hash.get(index);  
  42.         if (map.size() < pageSize) {   //判断当前桶是否满了  
  43.             map.put(key, value);  
  44.         } else {  
  45.             map.put(key, value);    
  46.             splitHash();              //满了就进行分裂  
  47.         }  
  48.     }  
  49.   
  50.     public int hashFun(int key, int f1) {  
  51.         return key % (4 * f1);  
  52.     }  
  53.   
  54.     public void splitHash() {  
  55.         Map<Integer, String> OldMap = hash.get(overPoint);   //旧桶  
  56.         Map<Integer, String> NewMap = new HashMap<Integer, String>(); //分裂产生的新桶  
  57.   
  58.         Integer[] keyList = OldMap.keySet().toArray(new Integer[0]);  
  59.         for (int i = 0; i < keyList.length; i++) {  //准备移动一半的数据到新桶  
  60.             int key = keyList[i].intValue();  
  61.             int index = hashFun(key, workRound + 1);  
  62.             if (index >= listSize) {  
  63.                 String value = OldMap.get(key);  
  64.                 OldMap.remove(key);  
  65.                 NewMap.put(key, value);  
  66.             }  
  67.         }  
  68.         hash.add(NewMap);  //将新桶放入哈希表  
  69.         listSize++;  //哈希表长度增加  
  70.         overPoint++;  //分裂点移动  
  71.         if(overPoint >= initlistSize){  //分裂点移动了一轮就更换新的哈希函数  
  72.             workRound++;  
  73.             initlistSize = initlistSize * 2;  
  74.             overPoint = 0;  
  75.         }  
  76.     }  
  77. }  


测试代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public class testLineHash {  
  2.     public static void main(String args[]){  
  3.         LineHash hash = new LineHash(3);  
  4.         hash.addKeyValue(4"this");  
  5.         hash.addKeyValue(8"is");  
  6.         hash.addKeyValue(12"a");  
  7.         hash.addKeyValue(16"test");  
  8.         hash.addKeyValue(20"!!!!");  
  9.         hash.addKeyValue(24"~~~");  
  10.         hash.addKeyValue(28"dsd");  
  11.         hash.addKeyValue(32"gg22");  
  12.           
  13.         for(int i = 4; i <= 24;)  
  14.         {  
  15.             System.out.println(hash.getKeyValue(i));  
  16.             i = i + 4;  
  17.         }  
  18.     }  




0 0
原创粉丝点击