Pages

Monday, May 16, 2011

算法总结系列之八:复读机的故事 - 散列表.NET应用的研究(下集)


估计写这么个题目会被扔鸡蛋, 因为实在是太大了. 各位不要期望太高啊,我写这东西,就是为了给自己个备忘. 你们要是把它当垃圾看, 说不定还能发现点什么东西.

言归正题. 说实话, .NET Framework的实现可能比我们认为的要好一些, 比如线程安全, 代码效率, 甚至以及代码风格方面. 比如HashTable 的实现就是一个比较好的佐证.

上文说到, .NET Framework 中的哈希表, 使用了双重散列的方式来计算散列值. 那么到底精确的来说, 是采用了什么关系式呢? 实际上很简单, 这个关系式就是:
h(key, n) = h1(key) + n*h2(key)


其中 
h1(key) = GetHash(key);        
h2(key) = 1 + ((h1(key) >> 5) + 1) % (hashsize - 1); 


key - 待散列的关键字值,
n - 发生碰撞的次数,
hashsize - 哈希表的容量,是一个素数.大牛Knuth同学在他那本<Art of Computer Programming>说过为什么要使用素数. .NET Framwork 兼顾效率和范围, 给出了这样一个实现:
internal static readonly int[] primes = {
3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919, 
1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, 
17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, 
1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369};
 
 
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
internal static int GetPrime(int min)
{
if (min < 0) 
throw new ArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow"));
for (int i = 0; i < primes.Length; i++) 
{
int prime = primes[i]; 
if (prime >= min) return prime;
}
 
//outside of our predefined table. 
//compute the hard way.
for (int i = (min | 1); i < Int32.MaxValue;i+=2) 
{ 
if (IsPrime(i))
return i; 
}
return min;
}
 
这段代码就是说,你要的容量如果在7199369之内,就从这个表里面给你返回一个最接近的素数; 否则, 给你找一个比想要容量大一点点的素数返回.
GetHash( Object )  - 是返回参数对象哈希值的函数, 它默认等于每个对象的GetHashCode()函数, 除非你制定了特定的IComparer对象,那么它会返回该Comparer的哈希值.哦? 原来GetHashCode()是用在这里啊... 我们知道每一个起源于Object的类,都会给IEqualityComparer接口一个默认的实现,这个接口的两个方法就是Equals()和GetHashCode().你当然可以重写GetHashCode()函数的实现, 但是这个函数直接关系到了这个类的对象放入HashTable以后, HashTable的效率表现. 默认的类方法GetHashCode()不至于太差, 如果你要实现自己的GetHashCode方法, 那么一定要考虑一下会不会和别的类生成的HashCode容不容易落入同一个值区间内. <Effective C#>这本书的第十个条目就解释了这样一个规律:
对于引用类型自带的GetHashCode函数来说,基本上是正确的,但是效率不高;而对于值类型自带的GetHashCode函数而言,基本上是不正确的,即使正确也是效率不高。如果重写类型的GetHashCode函数,想要达到既正确又高效是不可能的。
当然, 还有一条建议:
不建议重写此函数,而且在使用这个函数也需要先注意它的实现

每个哈希表都有装载率,装载率越高,空间越节省, 装载率越低, 查找越快.  .NET Framework的HashTable默认装载率是0.72, 但是表面上看来是1.0 . .NET的开发人员似乎为了引导我们使用他认为效率最为平衡的装载率, 偷偷的做了一下弊, 呵呵
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)); 
 
// Based on perf work, .72 is the optimal load factor for this table.
this.loadFactor = 0.72f * loadFactor;
double rawsize = capacity / this.loadFactor;
if (rawsize > Int32.MaxValue) 
throw new ArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow")); 
 
// Avoid awfully small sizes 
int hashsize = (rawsize > 11) ? HashHelpers.GetPrime((int)rawsize) : 11;
buckets = new bucket[hashsize];
 
loadsize = (int)(this.loadFactor * hashsize); 
isWriterInProgress = false;
// Based on the current algorithm, loadsize must be less than hashsize. 
BCLDebug.Assert( loadsize < hashsize, "Invalid hashtable loadsize!"); 
}

接下来的问题是, 我能计算出h(key, n)的值了, 然后这个元素会被放到哪里?  哈希表内部维护了一个key-value-hash code结构数组buckets, 每一个元素根据计算出的h(key, 1)值seed通过关系式(int) (seed % (uint)buckets.Length) 得到对应的数组索引, 如果发生碰撞则计算h(key, 2), 不碰撞就存入,依次类推.

针对容量不足和多次添加删除之后表内结构混乱, HashTable还提供了 expand() 和 rehash() 方法, 有兴趣的同学可以看看, 有点意思.
作者:Jeffrey Sun
出处:http://sun.cnblogs.com/
本文以“现状”提供且没有任何担保,同时也没有授予任何权利。本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

No comments:

Post a Comment