zoukankan      html  css  js  c++  java
  • 算法总结系列之八:复读机的故事 散列表.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/
    本文以“现状”提供且没有任何担保,同时也没有授予任何权利。本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    如何学习掌握一门新技术
    Linux多线程编程(不限Linux)
    腾讯后台开发面试题2
    腾讯后台开发面试题
    【转】Linux杀死fork产生的子进程的僵尸进程defunct
    【转】Linux网络编程入门
    【转】揭开Socket编程的面纱
    【转】简单理解socket
    【转】404、500、502等HTTP状态码介绍
    【转】fread函数和fwrite函数
  • 原文地址:https://www.cnblogs.com/sun/p/1258008.html
Copyright © 2011-2022 走看看