分享

Java的Hashtable实现

 moonboat 2011-08-17

最近做信息检索的VSM实验,字典生成这块用的是java自带的Hashtable数据结构,觉得效率还不错。后来有同学提到用词典树来保存字符串,可以用公共前缀来节约存储空间,最大限度的减少无谓的比较,查询效率要高于哈希表。(补充@2011.5.5 在数据较少的情况下,hash的查询效率应该是最高的,基本接近O(1),字典树的优势应该是在空间效率上)回头有时间研究下词典树的实现和分析,这里先分析一下Java的hashtable实现。

 


 

为了使用Eclipse去查看java本身的一些基础实现,我们需要先将java的源码加到Eclipse的jre路径中:

1.点 “window”-> "Preferences" -> "Java" -> "Installed JRES"

 

2.此时"Installed JRES"右边是列表窗格,列出了系统中的 JRE 环境,选择你的JRE,然后点边上的 "Edit..."

3.选中rt.jar文件,点右边的按钮“Source Attachment...”, 选择你的JDK目录下的“src.zip”文件即可

 


 

这样,在Eclipse中随便写一个Hashtable对象,然后ctrl单击就可以看到java的Hashtable类的实现了。下面这张是其总体的结构:

Java的Hashtable结构

总得来说就是每个哈希表都保存了一个Entry数组,然后每个Entry其实是存放碰撞的一个链,其中Entry类部分代码实现是:

  1. /** 
  2.      * Hashtable collision list. 
  3.      */  
  4.     private static class Entry<K,V> implements Map.Entry<K,V> {  
  5.     int hash;  
  6.     K key;  
  7.     V value;  
  8.     Entry<K,V> next;  
  9.     protected Entry(int hash, K key, V value, Entry<K,V> next) {  
  10.         this.hash = hash;  
  11.         this.key = key;  
  12.         this.value = value;  
  13.         this.next = next;  
  14.     }  
 

除了hash值和键值对,就是指向下一个Entry的“指针”了。哈希表还有两个主要的属性,一个是initialCapacity表示初始的大小,如果使用默认的构造函数,系统就设为11,注意这里容量不是可以存放字符串的个数,而是哈希的范围,设为11的话,所有的hash值都会映射到这11个位置上。另一个是loadFactor,表示存放元素的个数栈总的hash范围的比例,默认的是设为0.75,这是在空间和时间之间的一个权衡,如果过大,则会有很多的碰撞出现,搜索效率不高,而如果过低,则会占用很大的空间。还有一些其他的属性,比如总的元素个数,阈值等等,这里不再详述。

下面看下几个关键的函数实现,首先自然是put函数:

  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.     // Makes sure the key is not already in the hashtable.  
  7.     Entry tab[] = table;  
  8.     int hash = key.hashCode();  
  9.     int index = (hash & 0x7FFFFFFF) % tab.length;  
  10.     for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {  
  11.         if ((e.hash == hash) && e.key.equals(key)) {  
  12.         V old = e.value;  
  13.         e.value = value;  
  14.         return old;  
  15.         }  
  16.     }  
  17.     modCount++;  
  18.     if (count >= threshold) {  
  19.         // Rehash the table if the threshold is exceeded  
  20.         rehash();  
  21.             tab = table;  
  22.             index = (hash & 0x7FFFFFFF) % tab.length;  
  23.     }  
  24.     // Creates the new entry.  
  25.     Entry<K,V> e = tab[index];  
  26.     tab[index] = new Entry<K,V>(hash, key, value, e);  
  27.     count++;  
  28.     return null;  
  29.     }  

 

这里我们可以看到,对key的hash做了一个与操作,保证其是一个正整数,然后对数组的长度求余,得到索引,然后遍历这个索引位置的链表中的每一个元素,如果存在一个元素的key和插入的key相同,就修改其值。否则,就新建一个Entry放在index位置链表的最前面,其中用到了rehash函数,可以在当哈希表中的总个数超过当前容量乘以loadFactor(就是threshold)的时候,进行扩建和重排序:

  1. protected void rehash() {  
  2.     int oldCapacity = table.length;  
  3.     Entry[] oldMap = table;  
  4.     int newCapacity = oldCapacity * 2 + 1;  
  5.     Entry[] newMap = new Entry[newCapacity];  
  6.     modCount++;  
  7.     threshold = (int)(newCapacity * loadFactor);  
  8.     table = newMap;  
  9.     for (int i = oldCapacity ; i-- > 0 ;) {  
  10.         for (Entry<K,V> old = oldMap[i] ; old != null ; ) {  
  11.         Entry<K,V> e = old;  
  12.         old = old.next;  
  13.         int index = (e.hash & 0x7FFFFFFF) % newCapacity;  
  14.         e.next = newMap[index];  
  15.         newMap[index] = e;  
  16.         }  
  17.     }  
  18.     }  
 

 

容量扩大2倍加1,采用这个策略应该是有一定考虑的,我没有细究。在拷贝完之后,进行了一个重新的hash,因为容量已经变了,所以这个步骤是必须的。还有一些其他的函数,类似这里就不介绍了,最后我们来看下java的字符串hash是采用的什么算法:

  1. public int hashCode() {  
  2.     int h = hash;  
  3.     if (h == 0) {  
  4.         int off = offset;  
  5.         char val[] = value;  
  6.         int len = count;  
  7.             for (int i = 0; i < len; i++) {  
  8.                 h = 31*h + val[off++];  
  9.             }  
  10.             hash = h;  
  11.         }  
  12.         return h;  
  13.     }  
 

 

这个函数在String中,看上面非常简洁,就是对字符串中的每一个字符的ASCII码值进行的一个加和乘运算,乘数是31。这个算法是BKDR哈希算法,来自于Brian Kernighan 和 Dennis Ritchie的The C Programming Language一书,可以说是常用的hash算法中较为简洁的一个了,但是效率确实最好的之一,其中乘数的形式是31 131 1313 13131 131313...。关于常见的字符串hash算法,我会在以后的博客给予介绍,并用这次VSM的实验进行一个简单的测试。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多