通过简单的数学推导就可以得出其通项式公式即Hashtable的哈希函数簇为:
我们就拥有了一系列的哈希函数:,当我们向哈希表中增加元素时,则依次尝试使用这些哈希函数,直到找到相应的空闲内存单元地址为止,这种方式称为二度哈希。
在Hashtable类中,包含元素的存储桶被定义在结构体bucket中:
其中前两个字段很容易理解,分别代表了哈希表中的关键字和值,对于第三个字段hash_coll,实际上保存了两种信息:关键字的哈希码和是否冲突,coll为collision(冲突)的缩写,该字段为32位整型类型,Zui高位为符号位,当Zui高位为0时,表示该数为正数,表示未发生冲突,为1时为负数,表示发生了冲突,剩下的其他位则用于保存哈希码。
下面我们来看一个简单的哈希表元素增删过程,使得我们对于哈希表的具体工作方式有一个更直观的了解,当我们未指定具体Hashtable容量大小时,来进行一组数据的插入操作,此时Hashtable类会自动初始化其容量为默认Zui小值3。
插入元素[20,“elem1”],根据Hashtable类哈希函数通项式,其哈希代码的值为,此时为第一次插入数据,不存在冲突,直接寻址到bucket[2],由于不存在冲突,hash_coll的值即为其key的哈希代码,存储结构如下图:
插入元素[33,“elem2”],同理,此时仍然不存在冲突,存储结构如下:
插入元素[40,“elem3”],此时的哈希表进行扩容,为什么会在此时扩容呢,哈希表的填装因子为2/3=0.66并未超过0.72,在.NET中,微软对填装因子进行了换算,通过填装因子与哈希表大小的乘积取整获得哈希表的zuijia填装量即:3×0.72=2。扩容后的哈希表大小为原表容量大小的2倍后的质数,在本例中扩容后哈希表大小为7。进行扩容之后,原哈希表的已经存储的元素必须按照新的哈希表的哈希函数(其实哈希函数本身没有发生变动,发生变动的是哈希表的长度)进行计算,重新寻址,扩容后的哈希表如下:
完成扩容过程后才会对[40,“elem3”]进行插入操作,,现在我们发现冲突产生了,因为此时bucket[5]的位置已经有元素了,此时进行二度哈希:
此时哈希表中位置为1的空间仍旧处于空闲状态,进行插入操作,在将元素插入之前,由于bucket[5]出现了冲突,需要对其进行标记,将hash_coll的Zui高位置为1,表示其出现了冲突,完成插入后哈希表结构如下图:
插入元素[55,“elem4”],同理,,产生冲突,进行二度哈希:
,完成插入后哈希表的存储结构为:
删除元素[20,“elem1”],在删除元素时,同样需要根据哈希函数来进行寻址,如果有冲突,则进行二度哈希,值得注意的一点是,删除冲突标记元素(即元素的hash_coll值为负数)和非冲突标记元素是有差别的,在删除非冲突标记元素时,则直接将要删除的元素的键和值修改为null并将hash_coll置0即可,在删除冲突标记元素时,需将hash_coll的hash部分(即0-30位)置0以及将元素的值置为null,还需将该元素的键指向整个哈希表,之这样做是因为当索引为0的元素也出现冲突时,将无法判断该位置是一个空位还是非空位,那么进行插入时很可能将索引为0处的元素覆盖。删除[20,“elem1”]后的结构为: