hashtable
Hashtable
internal const Int32 HashPrime = 101;//一个固定的素数
private const Int32 InitialSize = 3;//哈希表的默认容量
private struct bucket {
public Object key;//键
public Object val;//值
public int hash_col;//哈希码,最高位是符号位,0:正整数,表示未发生冲突,1:负整数,表示发生冲突,存储的是h(key, i)的值而不是哈希地址
}
private bucket[] buckets;
private int count;//元素总数
private int occupancy;//冲突次数
private int loadsize;//相当于一个阈值,达到了这个数值,将对哈希表进行扩容
private float loadFactor;//哈希表中的元素占有数据桶空间的一个比率,这个值直接决定了哈希表在什么时候进行扩容。
private volatile int version;
private volatile bool isWriterInProgress;
private ICollection keys;
private ICollection values;
在Hashtable中的两个哈希函数分别为:
1. h_1(k) = k.GetHashCode():第一个哈希函数直接用默认的GetHashCode()方法;
2. h_2(k) = (1 + ((h_1(k) * HashPrime) % (hashsize - 1))):HashPrime为私有成员101的素数,hashsize为哈希表长度。之所以会进行取模运算是为了保证结果值的范围在[0, hashsize - 1]。
构建哈希算法的函数
Hashtable解决冲突使用了双重散列法,但又跟前面所讲的双重散列法稍有不同。它探测地址的方法如下:
h(key, i) = h1(key) + i * h2(key)
其中哈希函数h1和h2的公式如下:
h1(key) = key.GetHashCode()
h2(key) = 1 + (((h1(key) >> 5) + 1) % (hashsize - 1))
由于使用了二度哈希,最终的h(key, i)的值有可能会大于hashsize,所以需要对h(key, i)进行模运算,最终计算的哈希地址为:
哈希地址 = h(key, i) % hashsize
哈希表通过关键字查找元素时,首先计算出键的哈希地址,然后通过这个哈希地址直接访问数组的相应位置并对比两个键值,如果相同,则查找成功并返回;如果不同,则根据hash_coll的值来决定下一步操作。当hash_coll为0或正数时,表明没有冲突,此时查找失败;如果hash_coll为负数时,表明存在冲突,此时需通过二度哈希继续计算哈希地址进行查找,如此反复直到找到相应的键值表明查找成功,如果在查找过程中遇到hash_coll为正数或计算二度哈希的次数等于哈希表长度则查找失败。由此可知,将hash_coll的高位设为冲突位主要是为了提高查找速度,避免无意义地多次计算二度哈希的情况。
一,new一个Hashtable,Hashtable ht=new Hashtable();
Hashtable有多个构造函数,常用的是无参构造函数:Hashtable ht=new Hashtable(),在new一个hashtable时,其内部做了如下工作:调用Hashtable(int capacity,float loadFactor),其中capacity为:0,loadFactor为:1,然后初始化bocket数组大小为3,装载因子为0.72(该值是微软权衡后给的值),如下图所示,该图截取Reflector
二,向Hashtable添加一个元素,ht.Add("a","123")
1,判断当前Hashtable :ht的元素个数与bucket数组之比是否超过装载因子0.72,
1)小于0.72:对a进行哈希取值,然后将得到的值与bucket数组长度进行取模计算,将取模的结果插入到bucket数组对应索引,将“123”赋值其value.
因为哈希值可能会重复(不明白的百度一下),从而导致地址冲突,Hashtable 采用的是 "开放定址法" 处理冲突, 具体行为是把 HashOf(k) % Array.Length 改为 (HashOf(k) + d(k)) % Array.Length , 得出另外一个位置来存储关键字 "a" 所对应的数据, d 是一个增量函数. 如果仍然冲突, 则再次进行增量, 依此循环直到找到一个 Array 中的空位为止。
2) 大于0.72:对bucket数组进行扩容,
a, 新建一个数组(该数组的大小为两倍于当前容量最小的素数,比如当前数组长度是3,那么新数组长度为7)。
b, 将原来数组元素拷贝到新的数组中,因为bocket数组长度变了,所以需要对所有key重新哈希计算(这是影响hashtable性能的重要因素)。
c, 进行上面a步骤。
三,通过key获取Hashtable对应的value,var v=ht["a"];
1) 计算"a"的哈希值。
2)将计算的结果与bocket数组长度进行取模计算,因为哈希值可能会冲突,所以类似定位索引上的key可能与输入的key不相同,这时继续查找下一个位置。。。。。
3)取模计算结果即是存储在bocket数组上"123"的索引。
- Hashtable为什么查询速度快,而添加速度慢,且其添加和查询速度之比相差一个数量级?
Hashtable查询速度快是因为内部是基于数组索引定位的,稍微消耗性能的是取key’的哈希值,添加性能慢是因为添加元素可能会地址冲突,需要重新定位地址,扩容后数组拷贝,重新计算key。
- 装填因子(loadfactor)是什么,hashtable默认的装填因子时多少?
装填因子是hashtable中已存元素数/内部bucket数组长度,这个比值太大会造成冲突概率增大,太小会造成空间浪费,默认是0.72.
- Hashtable里的元素是顺序排序的吗?
不是顺序的
- Hashtable内部的数据桶默认长度是多少,其长度为什么只能是素数?
默认长度是3,因为素数有一个特点,只能被自己和1整除,如果不是素数,那么取模运算时可能会出现多个值。