Hashtable 也就是哈希表,是个非常重要的概率,在剖析hashtable源码前,我先简单介绍一下hashtable的原理

哈希表概念

什么是哈希(hash又称散列)?

将任意长度的消息压缩到某一固定长度的消息摘要的函数

什么是哈希表

给定一张表,通过哈希函数F(key)能将键值转化成表中的一个地址,便是哈希表

什么是哈希函数

能将键值转换为哈希表范围内的索引(0~M-1)

什么是哈希冲突

利用哈希函数计算地址时,不同的key值计算出了一样的结果,这种现象称为哈希冲突

怎么解决哈希冲突

  1. 开放定址法 :

    ​ 以发生哈希冲突的哈希地址为自变量,通过某种哈希函数得到一个新的空闲内存单元地址的方法

  2. 再哈希法 :

    ​ 发生冲突时,使用第二个、第三个、哈希函数计算地址,直到无冲突时

  3. 链地址法 :

    ​ 把产生冲突的数据元素另外存放在单链表中

HashHelpers

接下来我们开始读源码,尤其hashtable的源码经常使用HashHelpers这个辅助类,我先对HashHelpers类的几个关键方法进行介绍

public 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};

这是一张素数表,共72个元素,所有元素都是符合f(n) =1 +2n(n=1,2,….)的素数

public static bool IsPrime(int candidate) {if ((candidate & 1) != 0) {int limit = (int)Math.Sqrt (candidate);for (int divisor = 3; divisor <= limit; divisor+=2){if ((candidate % divisor) == 0)return false;}return true;}return (candidate == 2);}

IsPrime是用于判断candidate参数是否是素数

public 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;}for (int i = (min | 1); i < Int32.MaxValue;i+=2) {if (IsPrime(i) && ((i - 1) % 101 != 0))return i;}return min;}

GetPrime是用于获得一个素数,如果min小于预定表的最大数7199369就直接返回素数表中大于min数中最小的一个,如果超出预定表的最大数7199369,就按另一种复杂的方法判断。

不过这一种方法数学原理博主表示不能理解,不知道有哪位大神解释一下

hashtable

接下来进入主题,我们来先从hashtable的构造函数开始

构造原理

hashtable的构造函数需要2个参数,一个是容量,一个是装载因子。

装载因子是什么

装载因子哈希表是0.1 到 1.0 范围内的数字 ,是存储桶数(count)所占桶数组(buckets)桶数(hashsize)的最大比率 ,当桶数大于装载数(loadsize)时,桶数组就会扩容。

装载因子默认为1.0,但实际上微软将它乘以0.72,为什么是0.72呢,因为桶数组的过小 会导致空间的浪费,过大会导致哈希冲突的频繁发生,0.72是微软综合效率和内存损耗考虑,经过一番实验,取的一个比较均衡的结果

桶和桶数组

至于什么是桶和桶数组,看下面这段代码你就懂了,正是bucket桶这个类型存储hashtable的hash信息。至于桶数组存储了所有hashtable

private struct bucket {public Object key;public Object val;public int hash_coll;  }private bucket[] buckets;

构造

public Hashtable(int capacity) : this(capacity, 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));this.loadFactor = 0.72f * loadFactor;double rawsize = capacity / this.loadFactor;if (rawsize > Int32.MaxValue)throw new ArgumentException(Environment.GetResourceString("Arg_HTCapacityOverflow"));int hashsize = (rawsize > InitialSize) ? HashHelpers.GetPrime((int)rawsize) : InitialSize;buckets = new bucket[hashsize];loadsize = (int)(this.loadFactor * hashsize);isWriterInProgress = false;}

hashtable内部的构造函数内部主要做了什么呢

它利用 容量 /装载因子获取了一个桶数组大小的估值,借助HashHelpers.GetPrime获取了一个大于等于3的素数作为内部桶数组的初始长度,并以此长度初始桶数组

哈希算法函数

hashtable内部采用的是双重散列法,是开放地址法中最好的方法之一

private uint InitHash(Object key, int hashsize, out uint seed, out uint incr) {      uint hashcode = (uint) GetHash(key) & 0x7FFFFFFF;//取整数seed = (uint) hashcode;incr = (uint)(1 + ((seed * HashPrime) % ((uint)hashsize - 1)));return hashcode;}

我们可以看到函数利用out 关键词引用传递了seek和incr2个变量,seek就是key本身的哈希值,incr则是第二个哈希函数的增量

双重散列法简单介绍

就是有2个哈希函数,第一个哈希函数不行就用第二个,第二个就是之前计算的地址加上增量再mod m,再不行就继续用第二个哈希函数计算

双重散列法算法的使用

关于算法解释得可能不是很清楚,直接用算法的使用让大家更深刻的理解这个算法

public virtual bool ContainsKey(Object key) {if (key == null) {throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));}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 {b = lbuckets[bucketNumber];if (b.key == null) {return false;}if (((b.hash_coll &  0x7FFFFFFF) == hashcode) && KeyEquals (b.key, key))return true;bucketNumber = (int) (((long)bucketNumber + incr)% (uint)lbuckets.Length);                              } while (b.hash_coll < 0 && ++ntry < lbuckets.Length);return false;//哈希冲突数超出桶数,循环终止}

这是hashtable的ContainsKey方法,它完全是双重散列法算法的使用

利用第一个哈希函数计算bucketNumber

如果bucketNumber发现是冲突的地址,就利用第二个哈希函数继续计算地址,如果一直失败就持续为上次的·计算结果加上增量,至于% (uint)lbuckets.Length)是为将计算后的地址限制在哈希表的地址范围

元素添加和哈希冲突

private void putEntry (bucket[] newBuckets, Object key, Object nvalue, int hashcode)
{uint seed = (uint) hashcode;uint incr = (uint)(1 + ((seed * HashPrime) % ((uint)newBuckets.Length - 1)));int bucketNumber = (int) (seed % (uint)newBuckets.Length);            do {if ((newBuckets[bucketNumber].key == null) || (newBuckets[bucketNumber].key == buckets)) {newBuckets[bucketNumber].val = nvalue;newBuckets[bucketNumber].key = key;newBuckets[bucketNumber].hash_coll |= hashcode;return;}if( newBuckets[bucketNumber].hash_coll >= 0 ) {newBuckets[bucketNumber].hash_coll |= unchecked((int)0x80000000);occupancy++;}bucketNumber = (int) (((long)bucketNumber + incr)% (uint)newBuckets.Length);                } while (true);
}

这段代码是哈希表内部添加元素的代码,在哈希表利用key值查找地址,如果冲突,则occupancy自增,且利用双重散列法寻找下一个地址,不冲突则为桶数组对于地址的空桶赋值

哈希表的扩容

下面这段代码是哈希表的扩容函数,建立新的的桶数组,我们可以看到这段扩容代码在拷贝旧数据时不是简单的复制粘贴,而是利用putEntry重新添加元素,这般操作固然能保证之后哈希存取的效率,但大规模的putEntry终究是高时耗的,程序员使用hashtable时最好选择一个比较大的容量,避免重复扩容

private void rehash( int newsize, bool forceNewHashCode ) {occupancy=0;bucket[] newBuckets = new bucket[newsize];int nb;for (nb = 0; nb < buckets.Length; nb++){bucket oldb = buckets[nb];if ((oldb.key != null) && (oldb.key != buckets)) {int hashcode = ((forceNewHashCode ? GetHash(oldb.key) : oldb.hash_coll) & 0x7FFFFFFF);                              putEntry(newBuckets, oldb.key, oldb.val, hashcode);}}isWriterInProgress = true;buckets = newBuckets;loadsize = (int)(loadFactor * newsize);UpdateVersion();isWriterInProgress = false;return;}

插入条目

分析完hashtable的内部函数,我们再来看hashtable真正的插入元素操作

 private void Insert (Object key, Object nvalue, bool add) {if (key == null) {throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));}if (count >= loadsize) {expand();}else if(occupancy > loadsize && count > 100) {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;isWriterInProgress = true;                    buckets[bucketNumber].val = nvalue;buckets[bucketNumber].key  = key;buckets[bucketNumber].hash_coll |= (int) hashcode;count++;UpdateVersion();isWriterInProgress = false;   return;}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));}isWriterInProgress = true;                    buckets[bucketNumber].val = nvalue;UpdateVersion();                    isWriterInProgress = false; 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){isWriterInProgress = true;                    buckets[emptySlotNumber].val = nvalue;buckets[emptySlotNumber].key  = key;buckets[emptySlotNumber].hash_coll |= (int) hashcode;count++;UpdateVersion();                isWriterInProgress = false;     return;}throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_HashInsertFailed"));}

Insert的内部操作分三个步骤

  1. 判断key是否为空,如果空,哈希函数是无法计算地址的

  2. 判断条目数是否大于等于装载数,如果满足,按照之前对装载因子的介绍,哈希表需要扩容为原来的2倍以上;或者判断冲突数是否大于装载数,如果大于,则扩容

  3. 接着在循环体内按照双重散列法寻找对应键值的桶并为对应键值的桶赋值

删除条目

最后是hashtable解析的最后一部分,条目的删除

代码都是千篇一律的,核心就是按照双重散列法寻找对应键值的桶后将桶变成空桶

 public virtual void Remove(Object key) {if (key == null) {throw new ArgumentNullException("key", Environment.GetResourceString("ArgumentNull_Key"));}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);  rdo {b = buckets[bn];if (((b.hash_coll & 0x7FFFFFFF) == hashcode) && KeyEquals (b.key, key)) {isWriterInProgress = true;buckets[bn].hash_coll &= unchecked((int)0x80000000);if (buckets[bn].hash_coll != 0) {buckets[bn].key = buckets;} else {buckets[bn].key = null;}buckets[bn].val = null;  count--;UpdateVersion();isWriterInProgress = false; return;}bn = (int) (((long)bn + incr)% (uint)buckets.Length);                               } while (b.hash_coll < 0 && ++ntry < buckets.Length);}

转载于:https://www.cnblogs.com/millionsmultiplication/p/9409290.html

hashtable源码解析相关推荐

  1. 吊打java面试官之 Hashtable详细介绍(源码解析)和使用示例

    概要 前一章,我们学习了HashMap.这一章,我们对Hashtable进行学习. 我们先对Hashtable有个整体认识,然后再学习它的源码,最后再通过实例来学会使用Hashtable. 第1部分 ...

  2. 源码解析-深刻理解Hash HashTable HashMap原理及数据hash碰撞问题

    HashMap 前言 Hash HashTable 开地址法 线性探测法 平方探测法 双重散列探测法 拉链法 哈希表优势 HashMap 变量介绍 初始容量和负载因子 红黑树和链表转化 HashMap ...

  3. 万字长文|Hashtable源码深度解析以及与HashMap的区别

    基于JDK1.8对Java中的Hashtable集合的源码进行了深度解析,包括各种方法.扩容机制.哈希算法.遍历方法等方法的底层实现,最后给出了Hashtable和HashMap的详细对比以及使用建议 ...

  4. ConcurrentHashMap源码解析(1)

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 注:在看这篇文章之前,如果对HashMap的层不清楚的话,建议先去看看HashMap源码解析. http:/ ...

  5. unordered_map源码解析和个人理解

    参考链接 https://blog.csdn.net/ddkxddkx/article/details/6555754 还有这个源码解析https://zrj.me/archives/1248 1.下 ...

  6. The Wide and Deep Learning Model(译文+Tensorlfow源码解析) 原创 2017年11月03日 22:14:47 标签: 深度学习 / 谷歌 / tensorf

    The Wide and Deep Learning Model(译文+Tensorlfow源码解析) 原创 2017年11月03日 22:14:47 标签: 深度学习 / 谷歌 / tensorfl ...

  7. JVM类加载机制(ClassLoader)源码解析

    http://blog.csdn.net/chenyi8888/article/details/7066569 其实JVM类加载机制,简单地说就是类管理,也就是我们生成的class文件. 三个步骤:装 ...

  8. CopyOnWriteArrayList源码解析

    此文已由作者赵计刚授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 注:在看这篇文章之前,如果对HashMap的层不清楚的话,建议先去看看HashMap源码解析. http:/ ...

  9. 容器源码解析之HashMap(七)

    再进入源码解析之前,先来看看hashMap的工作原理 当我们执行put存值时,hashmap会先调用key的hashcode方法的到哈希码,也就是桶的索引bucketIndex,找到该桶,然后遍历桶用 ...

最新文章

  1. vs2008 中MessageBox()报错——CString与Char* 的转化
  2. Java内存溢出和内存泄露后怎么解决
  3. 关于 CSS3 backface-visiable 与 overflow 属性的冲突
  4. mysql行级安全_MySQL学习笔记(五):MySQL表级锁和行级锁
  5. 转行学Java,如何才能成为年薪50万的Java程序员呢?
  6. 1.计算机技术在音乐作品中的使用,数字技术在电子音乐中的应用论文
  7. 11-Elasticsearch的X-Pack
  8. 山东科技大学计算机网络_山东科技大学计算机网络共20讲视频
  9. 工作中遇到的问题——数据库
  10. TalkingData崔晓波:移动数据工具发展的三个阶段
  11. python实现算法改进_运动目标检测vibe算法及其改进Python实现
  12. 无火花工具的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
  13. 通过调用rundll32.exe来打开一些系统特定文件
  14. 蜂鸣器干扰通讯_提高蜂鸣器响度和降低蜂鸣器及驱动电路干扰电源的电路的制作方法...
  15. Java项目中使用Freemarker生成Word文档
  16. android 仿微信账单生成器手机版式,至尊版清理大师app
  17. 第三方互联网支付的主要类型
  18. PC端网站转换为webApp工具
  19. 当梵高遇上路飞——海贼王op风格迁移
  20. oppo安卓面试题,万字Android技术类校招面试题汇总,GitHub标星3.2K

热门文章

  1. QRadioButton分组且无边框的简单实现
  2. Gallery 之滑动速度的问题
  3. 单例模式 - 深究剖析
  4. DotNetSpeech.dll的使用
  5. react native native module
  6. Jmeter使用jp@gc-stepping thread group做性能测试
  7. 使用C#检验.NET FrameWork版本
  8. servlet设置session追踪模式
  9. flash build 4.6 不能debug 报错 C:\WINDOWS\system32\...
  10. Oracle 下 unpin 的cursor 才能被移除