算法总结系列之八:复读机的故事 - 散列表.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() 方法, 有兴趣的同学可以看看, 有点意思.

posted @ 2008-08-01 11:10  Jeffrey Sun  阅读(2656)  评论(2编辑  收藏  举报