今天来探究哈希表,.net内置的哈希表容器是Hashtable类,而Dictionary<TKey,TValue>是对应的泛型哈希表.
哈希表-Hashtable的实例化
一般我们实例化ArrayList或List<T>的时候,如果不指定容量,则其内部是赋值为一个静态的空数组。当有添加操作时,会实例化为一个长度为4的数组,如果容量满了以后,再添加,就会自动扩充为两倍的容量。
哈希表也有一个类似的情况,new Hashtable()如果不指定长度,则会将内置bucket数组的长度默认指定为11。如果给定一个值如new Hashtable(20),也并不会将bucket数组长度设置为20,而是循环内部一个素数数组,设置为刚好大于20的素数(也叫质数),此处是23。可知,哈希表内部存取数组长度总是一个素数。相关代码:
public Hashtable( int capacity, float loadFactor){ ...... //默认值为11 int num2 = (num > 11.0 ) ? HashHelpers.GetPrime(( int ) num) : 11 ; this .buckets = new bucket[num2]; ......}[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)] internal static int GetPrime( int min){ ......//循环素数数组找出刚好大于min的素数 for ( int i = 0 ; i < primes.Length; i ++ ) { int num2 = primes[i]; if (num2 >= min) { return num2; } } ......}//初始化素数数组 static HashHelpers(){ primes = new int [] { 3 , 7 , 11 , 0x11 , 0x17 , 0x1d , 0x25 , 0x2f , 0x3b , 0x47 , 0x59 , 0x6b , 0x83 , 0xa3 , 0xc5 , 0xef , 0x125 , 0x161 , 0x1af , 0x209 , 0x277 , 0x2f9 , 0x397 , 0x44f , 0x52f , 0x63d , 0x78b , 0x91d , 0xaf1 , 0xd2b , 0xfd1 , 0x12fd , 0x16cf , 0x1b65 , 0x20e3 , 0x2777 , 0x2f6f , 0x38ff , 0x446f , 0x521f , 0x628d , 0x7655 , 0x8e01 , 0xaa6b , 0xcc89 , 0xf583 , 0x126a7 , 0x1619b , 0x1a857 , 0x1fd3b , 0x26315 , 0x2dd67 , 0x3701b , 0x42023 , 0x4f361 , 0x5f0ed , 0x72125 , 0x88e31 , 0xa443b , 0xc51eb , 0xec8c1 , 0x11bdbf , 0x154a3f , 0x198c4f , 0x1ea867 , 0x24ca19 , 0x2c25c1 , 0x34fa1b , 0x3f928f , 0x4c4987 , 0x5b8b6f , 0x6dda89 };} 从源码可以得出一个结论:Hashtable总是创建一个比我们预想中要大一些的数组。
哈希表-Hashtable的存取方式
Hashtable内部包含了一个bucket类型的数组变量,bucket是一个结构,用来保存Key,Value和哈希值。
private struct bucket{ public object key; public object val; public int hash_coll;} public class Hashtable : // 相关接口 { private IEqualityComparer _keycomparer; private object _syncRoot; private bucket[] buckets; 从上面代码可以看出Hashtable跟ArrayList,List<T>一样,内部都是用Array数组进行存储数据。既然同样是数组,Hashtable和ArrayList存取方式有什么不同?
当Hashtable添加数据时,(例如:hash.Add(key)) 首先调用当前键值key的GetHashCode()方法,得到哈希值(该值为Int32),取该哈希值的绝对值跟内部数组(上面代码的buckets)的总容量进行余运算(就是'%'操作),得到一个小于总容量的值,把该值当做数组序号,存在对应的位置,通过Key获取数组序号的过程叫映射。
当然,两个不同的key值有可能的得到相同的序号,这叫哈希冲突,在此不做分析。
下面是部分源码:
private void Insert( object key, object nvalue, bool add){ uint num3 = this .InitHash(key, this .buckets.Length, out num, out num2); int num4 = 0 ; int index = - 1 ; //num6就是通过余计算得到的数组序号。 int num6 = ( int ) (num % this .buckets.Length); ......} private uint InitHash( object key, int hashsize, out uint seed, out uint incr){ //获取key在CLR中的HashCode值,并取绝对值。 uint num = ( uint ) ( this .GetHash(key) & 0x7fffffff ); seed = num; //此处针对哈希冲突 incr = 1 + (( uint ) (((seed >> 5 ) + 1 ) % (hashsize - 1 ))); return num;} 从Hashtable里取值的,也是通过映射Key值,找到数组中对应的存放索引位置,直接位置取出值,整个过程无需循环,时间复杂度为O(1),而ArrayList查找一个值则需要循环整个数组去逐个匹配,时间复杂度为O(n),由此可以看到哈希表在查询中的优势。
既然Hashtable本身也是数组存储,查询速度又快,那还要ArrayList做什么?存储数据的时候都用Hashtable不就行了?有句话说,XX打开了一扇门,就必然要关闭很多窗,现在分析一下Hashtable关闭的那些窗。
首先,哈希表不能有重复键值。
其次,哈希表会占用更多内存空间。
第一点显而易见,现在分析第二点。
哈希表-Hashtable添加时的内部扩充方式
我们知道ArrayList和List<T>在添加数值时,如果容量不足,会新建一个二倍容量的数组,然后把原数组数据复制过去,达到容量扩充的目的,所以使用ArrayList和List <T>的时候,建议在实例化方法里指定容量。
那么Hashtable在添加操作时碰到内部数组容量不足会怎么做?
其实处理方法和ArrayList基本相似,也是二倍扩充,但略有不同。不同点在于:
首先,扩充的大小比二倍略大。上面在实例化Hashtable那一部分中提到,由于哈希算法本身的特点,数组长度总是一个素数,所以当需要扩充时,新的数组长度是刚好大于原数组二倍长度的素数,也就是说略大于二倍。
其次,数组在未存满的情况下就要扩充。Hashtable有一个加载因子为0.72f。实际能存储的数据量等于内部Array长度* 0.72f。也就是说一个长度为11的数组,当存了7个数据以后,就不能再存储,需要要扩充容量。代码如下:
public Hashtable( int capacity, float loadFactor){ ......//初始状态loafFactor=1f this .loadFactor = 0.72f * loadFactor;...... int num2 = (num > 11.0 ) ? HashHelpers.GetPrime(( int ) num) : 11 ; this .buckets = new bucket[num2]; //实际可加载量 this .loadsize = ( int ) ( this .loadFactor * num2);......} //Hashtable的Add方法内部调用了Insert方法 private void Insert( object key, object nvalue, bool add){ ...... if ( this .count >= this .loadsize) { //内部容量扩充的方法 this .expand(); } ......} 也就是说在Hashtable中,假如初始化了100个位置,只有72个可用,其他28个不能用,但仍然要占着地方。
由此我们可以知道,如果保存的数据量相同,Hashtable要比ArrayList占用更多的内存空间。
哈希表-泛型Dictionary<TKey,TValue>和Hashtable比较
.net 2.0之后出现了泛型,泛型避免了装箱拆箱造成的损失,在大多数情况下,使用泛型集合要比传统的集合有较高的性能。List<T>对应ArrayList,Dictionary<TKey,TValue>对应Hashtable。
Dictionary<TKey,TValue>除了泛型带来的优势以外,还在哈希值算法,哈希冲突检测上做了改进,也没有了100个位置28个不能用的空间浪费。所以当碰到哈希表使用时,优先考虑Dictionary<TKey,TValue>。
最后总结:哈希表在查询方面有明显优势,但会占用多一点的内存空间,当业务中的集合有大量的查找操作时,优先考虑哈希表,一旦决定使用哈希表,优先考虑泛型哈希表。