源代码版本为 .NET Framework 4.6.1
有投入,有产出。
Hashtable实现一个哈希表(也叫散列表),将键映射到相应的值。任何非 null 对象都可以用作键。
哈希表的实现比较复杂,最好先了解一下相关的方法和概念。
(注:非基础性,主要涉及Hashtable的实现原理)
水平有限,若有不对之处,望指正。
哈希表的概念
哈希表是根据关键码值进行访问的数据结构,它是通过把关键码值映射到表中对应的一个位置来访问记录值,以加快查询速度(给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。)。
什么是Hash
Hash,一般翻译做“散列”,也有直接音译为“哈希”的,就是把任意长度的输入(又叫做预映射),通过散列算法,变换成固定长度的输出,该输出就是散列值。这种转换是一种压缩映射,也就是,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,而不可能从散列值来唯一的确定输入值。简单的说就是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数。
哈希冲突
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素。
哈希函数的目标是尽量减少冲突,但实际应用中冲突是无法避免的,所以在冲突发生时,必须有相应的解决方案。而发生冲突的可能性又跟以下两个因素有关:
1. 装填因子α:所谓装填因子是指合希表中已存入的记录数n与哈希地址空间大小m的比值,即 α=n / m ,α越小,冲突发生的可能性就越小;α越大,冲突发生的可能性就越大(α取值范围0.1f ~ 1.0f)。这很容易理解,因为α越小,哈希表中空闲单元的比例就越大,所以待插入记录同已插入的记录发生冲突的可能性就越小;反之,α越大,哈希表中空闲单元的比例就越小,所以待插入记录同已插入记录冲突的可能性就越大;另一方面,α越小,存储桶的利用率就越低;反之,存储桶的利用率就越高。为了既兼顾减少冲突的发生,又兼顾提高存储空间的利用率,通常把α控制在0.6~0.9的范围之内,C#的HashTable类把α的值定为0.72。
2. 与所采用的哈希函数有关。若哈希函数选择得当,就可使哈希地址尽可能均匀地分布在哈希地址空间上,从而减少冲突的发生;否则,就可能使哈希地址集中于某些区域,从而加大冲突发生的可能性。
哈希冲突解决
冲突解决技术可分为两大类:开散列法(又称为链地址法)和闭散列法(又称为开放地址法)。哈希表是用数组实现的一片连续的地址空间,两种冲突解决技术的区别在于发生冲突的元素是存储在这片数组的空间之外还是空间之内(一个数组空间或多个数组空间):
1. 开散列法发生冲突的元素存储于数组空间之外。可以把“开”字理解为需要另外“开辟”空间存储发生冲突的元素。
2. 闭散列法发生冲突的元素存储于数组空间之内。可以把“闭”字理解为所有元素,不管是否有冲突,都“关闭”于数组之中。闭散列法又称开放地址法,意指数组空间对所有元素,不管是否冲突都是开放的。
闭散列法(开放地址法)
闭散列法是把所有的元素存储在哈希表数组中。当发生冲突时,在冲突位置的附近寻找可存放记录的空单元。寻找“下一个”空位的过程称为探测。上述方法可用如下公式表示:
hi=( h(key) + di ) % m i=1,2,…,k (k≤m-1)
其中h(key)为哈希函数;m为哈希表长;di为增量的序列。根据di取值的不同,可以分成几种探测方法,下面介绍的是Hashtable所使用到的双重散列法。
双重散列法(DoubleHashing)
双重散列法是经典的数据表结构(T)。设 n 为存储在 T 中元素的数目,m为T的容量,则T的加载因子为
α= n / m, α:1 > α >0。
它是以关键字的另一个散列函数值作为增量。设两个哈希函数为:h_1 和 h_2,则得到的探测序列为:
h(i,k) = ( h_1(k) + i * h_2(k) ) % m,m为哈希表的容量,i: 1 < i < m - 1。
定义h_2的方法较多,但无采用什么方法都必须使h_2(k)的值和m互素(又称互质,表示两数的最大公约数为1,或者说是两数没有共同的因子,1除外)才能使发生冲突的同义词地址均匀地分布在整个哈希表中,否则可能造成同义词地址的循环计算。若m为素数,则h_2取1至m-1之间的任何数均与m互素。
Hashtable的实现
Hashtable实现了IDictionary,在命名空间System.Collections中,表示根据键的哈希代码进行组织的键/值对的集合。
internal const Int32 HashPrime = 101;
private const Int32 InitialSize = 3;
private struct bucket {
public Object key;//键
public Object val;//值
public int hash_col;//哈希码
}
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;
HashPrime:是一个固定的素数;
InitialSize :是哈希表的默认容量;
count :记录哈希表中的元素总数;
occupancy: 记录哈希表发生冲突的次数;
loadsize: 装载容量值,相当于一个阈值,达到了这个数值,将对哈希表进行扩容;
loadFactor: 哈希表中的元素占有数据桶空间的一个比率,这个比例直接决定了哈希表在什么时候进行扩容;
buckets:称为数据桶,用于存储哈希表中的元素,它是一个结构体,包含:
1. key:键,键是不能重复的;
2. val:值,可以是任何的类型(想要类型安全可以选择Dictionary,是Hashtable的泛型实现);
3. hash_col:是一个Int32类型,它的最高位是符号位,为“0”时,表示这是一个正整数;为“1”时表示负整数。hash_coll使用最高位表示当前位置是否发生冲突,正数表示未发生冲突;负数表示当前位置存在冲突。之所以专门使用一个位用于存放哈希码并标注是否发生冲突,主要是为了提高哈希表的运行效率。
在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的构造函数很多,这里记录一个最核心的构造函数
Hashtable(Int32, Single):使用指定的初始容量、指定的加载因子、默认的哈希代码提供程序和默认比较器来初始化 Hashtable 类的新的空实例。有两个主要参数:
1. capacity:最初可包含的元素的近似数目。
2. loadFactor:0.1 到 1.0 范围内的数字,再乘以提供最佳性能的默认值0.72f。结果是元素与存储桶的最大比率,建议该值使用默认的1.0f,因为该值越小,越会造成空间的浪费。
public Hashtable(int capacity, float loadFactor){
if (capacity < 0)
throw new ArgumentOutOfRangeException("capacity",
Environment.GetResourceString("ArgumentOutOfRange_NeedNonNegNum"));
if (!(loadFactor >= 0.1f && loadFactor <= 1.0f))
throw new ArgumentOutOfRangeException("loadFactor",
Environment.GetResourceString("ArgumentOutOfRange_HashtableLoadFactor", .1, 1.0));
Contract.EndContractBlock();
//官方的备注是 0.72f为最优加载因子
this.loadFactor = 0.72f * loadFactor;
//原始容量
double rawsize = capacity / this.loadFactor;
//容量不超过 Int32.MaxValue
if (rawsize > Int32.MaxValue)
throw new ArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow"));
//容量大于等于默认容量值
int hashsize = (rawsize > InitialSize) ? HashHelpers.GetPrime((int)rawsize) : InitialSize;
//数据桶,是HashTable内部维护的关键数据
buckets = new bucket[hashsize];
//装载容量,结果为小于或等于容量的值
loadsize = (int)(this.loadFactor * hashsize);
isWriterInProgress = false;
Contract.Assert( loadsize < hashsize, "Invalid hashtable loadsize!");
}
构建哈希算法的函数
函数里包含了双重散列法的哈希函数和增量
private uint InitHash(Object key, int hashsize, out uint seed, out uint incr) {
//取正数值,第一和哈希函数h_1(k)
uint hashcode = (uint) GetHash(key) & 0x7FFFFFFF;
seed = (uint) hashcode;
//第二个哈希函数h_2(k)的增量
incr = (uint)(1 + ((seed * HashPrime) % ((uint)hashsize - 1)));
return hashcode;
}
添加元素
Add(Object, Object):将带有指定键和值的元素添加到 Hashtable 中。
emptySlotNumber : 记录第一个寻址到的可用插槽。
在Add(Object, Object)中,有一个do while循环,用于元素的插入验证。思路分析:
public virtual void Add(Object key, Object value) {
Insert(key, value, true);
}
private void Insert (Object key, Object nvalue, bool add) {
if (key == null) {
throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));
}
Contract.EndContractBlock();
if (count >= loadsize) {
//当元素的总数大于等于装载量时,自动扩容
expand();
}
else if(occupancy > loadsize && count > 100) {
//在元素总数大于100之后,判断冲突计数大于装载量时,将HashTable重新哈希
rehash();
}
uint seed;
uint incr;
uint hashcode = InitHash(key, buckets.Length, out seed, out incr);
int ntry = 0;//寻址次数,不得大于等于哈希表容量
int emptySlotNumber = -1; //用于记录第一个寻址到的可用插槽
int bucketNumber = (int) (seed % (uint)buckets.Length);
do {
//有冲突的空插槽
if (emptySlotNumber == -1 && (buckets[bucketNumber].key == buckets) && (buckets[bucketNumber].hash_coll < 0))
emptySlotNumber = bucketNumber;
//正常的空插槽
if ((buckets[bucketNumber].key == null) ||
(buckets[bucketNumber].key == buckets && ((buckets[bucketNumber].hash_coll & unchecked(0x80000000))==0))) {
//将元素放入寻址到的第一个可用插槽
if (emptySlotNumber != -1)
bucketNumber = emptySlotNumber;
Thread.BeginCriticalRegion();
isWriterInProgress = true;
buckets[bucketNumber].val = nvalue;
buckets[bucketNumber].key = key;
buckets[bucketNumber].hash_coll |= (int) hashcode;
count++;
UpdateVersion();
isWriterInProgress = false;
Thread.EndCriticalRegion();
if(ntry > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(_keycomparer))
{
if(_keycomparer == null || !(_keycomparer is System.Collections.Generic.RandomizedObjectEqualityComparer))
{
_keycomparer = HashHelpers.GetRandomizedEqualityComparer(_keycomparer);
rehash(buckets.Length, true);
}
}
return;
}
//替换更新(此处值变更,Update操作),若添加重复的键,则抛出异常
if (((buckets[bucketNumber].hash_coll & 0x7FFFFFFF) == hashcode) &&
KeyEquals (buckets[bucketNumber].key, key)) {
if (add) {
throw new ArgumentException(Environment.GetResourceString("Argument_AddingDuplicate__", buckets[bucketNumber].key, key));
}
Thread.BeginCriticalRegion();
isWriterInProgress = true;
buckets[bucketNumber].val = nvalue;
UpdateVersion();
isWriterInProgress = false;
Thread.EndCriticalRegion();
if(ntry > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(_keycomparer))
{
if(_keycomparer == null || !(_keycomparer is System.Collections.Generic.RandomizedObjectEqualityComparer))
{
_keycomparer = HashHelpers.GetRandomizedEqualityComparer(_keycomparer);
rehash(buckets.Length, true);
}
}
return;
}
//存在冲突 将哈希值设置为负数
if (emptySlotNumber == -1) {
if( buckets[bucketNumber].hash_coll >= 0 ) {
buckets[bucketNumber].hash_coll |= unchecked((int)0x80000000);
occupancy++;
}
}
bucketNumber = (int) (((long)bucketNumber + incr)% (uint)buckets.Length);
//寻址次数肯定是不能超过最大索引下标的,此处循环用于冲突的二次寻址
} while (++ntry < buckets.Length);
//插槽已满,将元素插入第一个寻址到的可用插槽
if (emptySlotNumber != -1)
{
Thread.BeginCriticalRegion();
isWriterInProgress = true;
buckets[emptySlotNumber].val = nvalue;
buckets[emptySlotNumber].key = key;
buckets[emptySlotNumber].hash_coll |= (int) hashcode;
count++;
UpdateVersion();
isWriterInProgress = false;
Thread.EndCriticalRegion();
if(buckets.Length > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(_keycomparer))
{
if(_keycomparer == null || !(_keycomparer is System.Collections.Generic.RandomizedObjectEqualityComparer))
{
_keycomparer = HashHelpers.GetRandomizedEqualityComparer(_keycomparer);
rehash(buckets.Length, true);
}
}
return;
}
Contract.Assert(false, "hash table insert failed! Load factor too high, or our double hashing function is incorrect.");
throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_HashInsertFailed"));
}
获取值 / 设置值
索引器:Hashtable中通过索引器来进行获取值 / 设置值。
哈希表的读的操作有三个步骤 ︰
(1) 计算哈希值和找到的插槽号。
(2) 比较哈希码,如果相等,请转至步骤 3。否则读失败,结束。
(3) 比较关键字,如果相等,返回包含在存储桶中的值。否则读失败,结束。
在索引器的源代码中 有两个do while循环,最外面的循环用于遍历冲突链,嵌套的循环用于防止数据读脏。
public virtual Object this[Object key] {
get {
if (key == null) {
throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));
}
Contract.EndContractBlock();
uint seed;
uint incr;
//生成一个数据桶的结构副本,防止其他线程同一时间对同一个结构进行调整。
bucket[] lbuckets = buckets;
uint hashcode = InitHash(key, lbuckets.Length, out seed, out incr);
int ntry = 0;
bucket b;
int bucketNumber = (int) (seed % (uint)lbuckets.Length);
do
{
int currentversion;
int spinCount = 0;
do {
currentversion = version;
b = lbuckets[bucketNumber];
//这里使用线程休眠是为了防止资源争夺而导致CPU过度消耗
if( (++spinCount) % 8 == 0 ) {
Thread.Sleep(1);
}
//若有其他线程在做调整,等待完成再获取最新的值
} while ( isWriterInProgress || (currentversion != version) );
if (b.key == null) {
return null;
}
if (((b.hash_coll & 0x7FFFFFFF) == hashcode) &&
KeyEquals (b.key, key))
return b.val;
bucketNumber = (int) (((long)bucketNumber + incr)% (uint)lbuckets.Length);
} while (b.hash_coll < 0 && ++ntry < lbuckets.Length);
return null;
}
set {
//更新现在的键值
Insert(key, value, false);
}
}
//比较函数
protected virtual bool KeyEquals(Object item, Object key)
{
Contract.Assert(key != null, "key can't be null here!");
if( Object.ReferenceEquals(buckets, item)) {
return false;
}
if (Object.ReferenceEquals(item,key))
return true;
if (_keycomparer != null)
return _keycomparer.Equals(item, key);
return item == null ? false : item.Equals(key);
}
移除元素
Remove(Object):从 Hashtable 中移除带有指定键的元素。
Hashtable删除元素 分两种情况处理:
(1) 正常插槽,将key赋空引用,hash_col赋值0。
(2)冲突插槽,将key指向buckets数据桶,将hash_col赋值-2147483648 (同时赋值key和hash_col是为了与哈希码为0的冲突插槽区分开)。
public virtual void Remove(Object key) {
if (key == null) {
throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));
}
Contract.EndContractBlock();
Contract.Assert(!isWriterInProgress, "Race condition detected in usages of Hashtable - multiple threads appear to be writing to a Hashtable instance simultaneously! Don't do that - use Hashtable.Synchronized.");
uint seed;
uint incr;
uint hashcode = InitHash(key, buckets.Length, out seed, out incr);
int ntry = 0;
bucket b;
int bn = (int) (seed % (uint)buckets.Length);
//第一次循环若找不到值,那么表示有冲突链 或 键值不存在
do {
b = buckets[bn];
if (((b.hash_coll & 0x7FFFFFFF) == hashcode) && KeyEquals (b.key, key)) {
Thread.BeginCriticalRegion();
isWriterInProgress = true;
//正常插槽哈希码为0 / 冲突插槽哈希码为负数
buckets[bn].hash_coll &= unchecked((int)0x80000000);
if (buckets[bn].hash_coll != 0) {
//冲突插槽的key指向buckets
buckets[bn].key = buckets;
}
else {
//正常插槽的key赋空引用
buckets[bn].key = null;
}
buckets[bn].val = null;
count--;
UpdateVersion();
isWriterInProgress = false;
Thread.EndCriticalRegion();
return;
}
bn = (int) (((long)bn + incr)% (uint)buckets.Length);
//循环冲突链
} while (b.hash_coll < 0 && ++ntry < buckets.Length);
}
最后
加载因子确定元素与存储桶的最大比率。较小的加载因素会导致更快地平均查找时间,但这样做会增加的内存消耗。默认的加载因子 1.0 通常提供速度和大小之间的最佳平衡。
随着哈希表的元素数量不断增加,实际的装载因子(装载量)也会随着增大。当哈希表中元素的数量达到装载因子的数值时,哈希表的容量将自动增加到大于当前数的两倍的最小质数。