Java Hashtable分析与冲突处理

Hashtable的结构,采用的是数据结构中所说的链地址法处理冲突的方法 

 

/**
     * Hashtable collision list.
     */
    private static class Entry<K,V> implements Map.Entry<K,V> {
int hash;
K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
   this.hash = hash;
   this.key = key;
   this.value = value;
   this.next = next;
}

}

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

下面看下几个关键的函数实现,首先自然是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;
   }
}
modCount++;
if (count >= threshold) {
   // Rehash the table if the threshold is exceeded
   rehash();
            tab = table;
            index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
Entry<K,V> e = tab[index];
tab[index] = new Entry<K,V>(hash, key, value, e);
count++;
return null;
    }


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


protected void rehash() {
int oldCapacity = table.length;
Entry[] oldMap = table;
int newCapacity = oldCapacity * 2 + 1;
Entry[] newMap = new Entry[newCapacity];
modCount++;
threshold = (int)(newCapacity * loadFactor);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
   for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = newMap[index];
newMap[index] = e;
   }
}
    }

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


public int hashCode() {
int h = hash;
if (h == 0) {
   int off = offset;
   char val[] = value;
   int len = count;
            for (int i = 0; i < len; i++) {
                h = 31*h + val[off++];
            }
            hash = h;
        }
        return h;
    }


从上面的结构图可以看出,Hashtable的实质就是一个数组+链表。图中的Entry就是链表的实现,Entry的结构中包含了对自己的另一个实例的引用next,用以指向另外一个Entry。而图中标有数字的部分是一个Entry数组,数字就是这个Entry数组的index。那么往 Hashtable增加键值对的时候,index会根据键的hashcode、Entry数组的长度共同决定,从而决定键值对存放在Entry数组的哪个位置。从这种意义来说,当键一定,Entry数组的长度一定的情况下,所得到的index肯定是相同的,也就是说插入顺序应该不会影响输出的顺序才对。然而,还有一个重要的因素没有考虑,就是计算index出现相同值的情况。譬如代码中 "sichuan" 和 "anhui",所得到的index是相同的,在这个时候,Entry的链表功能就发挥作用了:put方法通过Entry的next属性获得对另外一个 Entry的引用,然后将后来者放入其中。根据debug得出的结果,"sichuan", "anhui"的index同为2,"hunan"的index为6,"beijing"的index为1,在输出的时候,会以index递减的方式获得键值对。很明显,会改变的输出顺序只有"sichuan"和"anhui"了,也就是说输出只有两种可能:"hunan" - "sichuan" - "anhui" - "beijing"和"hunan" - "anhui" - "sichuan" - "beijing"。以下是运行了示例代码之后,Hashtable的结果: 
 
在Hashtable的实现代码中,有一个名为rehash的方法用于扩充Hashtable的容量。很明显,当rehash方法被调用以后,每一个键值对相应的index也会改变,也就等于将键值对重新排序了。这也是往不同容量的Hashtable放入相同的键值对会输出不同的键值对序列的原因。在Java中,触发rehash方法的条件很简单:hahtable中的键值对超过某一阀值。默认情况下,该阀值等于hashtable中Entry数组的长度×0.75。 



自 Java 2 平台 v1.2 以来,此类已经改进为可以实现 Map,因此它变成了 Java Collections Framework 的一部分。与新集合的实现不同,Hashtable 是同步的。 

由迭代器返回的 Iterator 和由所有 Hashtable 的“collection 视图方法”返回的 Collection 的 listIterator 方法都是快速失败 的:在创建 Iterator 之后,如果从结构上对 Hashtable 进行修改,除非通过 Iterator 自身的移除或添加方法,否则在任何时间以任何方式对其进行修改,Iterator 都将抛出 ConcurrentModificationException。因此,面对并发的修改,Iterator 很快就会完全失败,而不冒在将来某个不确定的时间发生任意不确定行为的风险。由 Hashtable 的键和值方法返回的 Enumeration 不 是快速失败的。 

注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出 ConcurrentModificationException。因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误做法:迭代器的快速失败行为应该仅用于检测程序错误。 
直接看HashTable.java  
解决哈希表的冲突-开放地址法和链地址法 
在实际应用中,无论如何构造哈希函数,冲突是无法完全避免的。 

1 开放地址法 


这个方法的基本思想是:当发生地址冲突时,按照某种方法继续探测哈希表中的其他存储单元,直到找到空位置为止。这个过程可用下式描述: 
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 = 伪随机序列 伪随机再散列; 

例1设有哈希函数 H ( key ) = key mod 7 ,哈希表的地址空间为 0 ~ 6 ,对关键字序列( 32 , 13 , 49 , 55 , 22 , 38 , 21 )按线性探测再散列和二次探测再散列的方法分别构造哈希表。 
解: 
( 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 
   注意:对于利用开放地址法处理冲突所产生的哈希表中删除一个元素时需要谨慎,不能直接地删除,因为这样将会截断其他具有相同哈希地址的元素的查找地址,所以,通常采用设定一个特殊的标志以示该元素已被删除。 
2 链地址法 

链地址法解决冲突的做法是:如果哈希表空间为 0 ~ m - 1 ,设置一个由 m 个指针分量组成的一维数组 ST[ m ], 凡哈希地址为 i 的数据元素都插入到头指针为 ST[ i ] 的链表中。这种方法有点近似于邻接表的基本思想,且这种方法适合于冲突比较严重的情况。 

例 2 设有 8 个元素 { a,b,c,d,e,f,g,h } ,采用某种哈希函数得到的地址分别为: {0 , 2 , 4 , 1 , 0 , 8 , 7 , 2} ,当哈希表长度为 10 时,采用链地址法解决冲突的哈希表如下图所示。 
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值