.net集合类的研究-哈希表(一)--Hashtable,Dictionary

今天来探究哈希表,.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>。

最后总结:哈希表在查询方面有明显优势,但会占用多一点的内存空间,当业务中的集合有大量的查找操作时,优先考虑哈希表,一旦决定使用哈希表,优先考虑泛型哈希表。

你可能感兴趣的:(.net集合类的研究-哈希表(一)--Hashtable,Dictionary)