C# Dictionary源码剖析
参考:https://blog.csdn.net/exiaojiu/article/details/51252515
http://www.cnblogs.com/wangjun1234/p/3719635.html
源代码版本为 .NET Framework 4.6.1
Dictionary是Hashtable的一种泛型实现(也是一种哈希表)实现了IDictionary泛型接口和非泛型接口等,将键映射到相应的值。任何非 null 对象都可以用作键。使用与Hashtable不同的冲突解决方法,Dictionary使用拉链法。
概念重播
对于不同的关键字可能得到同一哈希地址,即key1 != key2 => F(key1)=F(fey2),这种现象叫做冲突,在一般情况下,冲突只能尽可能的少,而不能完全避免。因为,哈希函数是从关键字集合到地址集合的映像。通常,关键字集合比较大,它的元素包括很多有可能的关键字。既然如此,那么,如何处理冲突则是构造哈希表不可缺少的一个方面。
通常用于处理冲突的方法有:开放定址法、再哈希法、链地址法、建立一个公共溢出区等。
在哈希表上进行查找的过程和哈希造表的过程基本一致。给定K值,根据造表时设定的哈希函数求得哈希地址,若表中此位置没有记录,则查找不成功;否则比较关键字,若和给定值相等,则查找成功;否则根据处理冲突的方法寻找“下一地址”,只到哈希表中某个位置为空或者表中所填记录的关键字等于给定值时为止。
哈希函数
Dictionary使用的哈希函数是除留余数法,在源码中的公式为:
h = F(k) % m; m 为哈希表长度(这个长度一般为素数)
(内部有一个素数数组:3,7,11,17....如图:);
通过给定或默认的GetHashCode()函数计算出关键字的哈希码模以哈希表长度,计算出哈希地址。
拉链法
Dictionary使用的解决冲突方法是拉链法,又称链地址法。
拉链法的原理:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。结构图大致如下:
特别强调:拉链法只是使用链表的原理去解决冲突,并不是真的有一个链表存在。
基本成员
private struct Entry {
public int hashCode; //31位散列值,32最高位表示符号位,-1表示未使用
public int next; //下一项的索引值,-1表示结尾
public TKey key; //键
public TValue value; //值
} private int[] buckets;//内部维护的数据地址
private Entry[] entries;//元素数组,用于维护哈希表中的数据
private int count;//元素数量
private int version;
private int freeList;//空闲的列表
private int freeCount;//空闲列表元素数量
private IEqualityComparer<TKey> comparer;//哈希表中的比较函数
private KeyCollection keys;//键集合
private ValueCollection values;//值集合
private Object _syncRoot;
buckets 就想在哈希函数与entries之间解耦的一层关系,哈希函数的F(k)变化不在直接影响到entries。
freeList 类似一个单链表,用于存储被释放出来的空间即空链表,一般有被优先存入数据。
freeCount 空链表的空位数量。
初始化函数
该函数用于,初始化的数据构造
private void Initialize(int capacity) {
//根据构造函数设定的初始容量,获取一个近似的素数
int size = HashHelpers.GetPrime(capacity);
buckets = new int[size];
for (int i = ; i < buckets.Length; i++) buckets[i] = -;
entries = new Entry[size];
freeList = -;
}
初始化Dictionary内部数组容器:buckets int[]和entries<T,V>[],分别分配长度3。
(内部有一个素数数组:3,7,11,17....如图:);
size 哈希表的长度是素数,可以使元素更均匀地分布在每个节点上。
buckets 中的节点值,-1表示空值。
freeList 为-1表示没有空链表。
buckets 和 freeList 所值指向的数据其实全是存储于一块连续的内存空间(entries )之中。
插入元素
public void Add(TKey key, TValue value) {
Insert(key, value, true);
}
private void Insert(TKey key, TValue value, bool add){
if( key == null ) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
} if (buckets == null) Initialize();
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int targetBucket = hashCode % buckets.Length; //循环冲突
for (int i = buckets[targetBucket]; i >= ; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
if (add) {
ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
}
entries[i].value = value;
version++;
return;
} collisionCount++;
} //添加元素
int index;
//是否有空列表
if (freeCount > ) {
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else {
if (count == entries.Length)
{
Resize();//自动扩容
targetBucket = hashCode % buckets.Length;//哈希函数寻址
}
index = count;
count++;
} entries[index].hashCode = hashCode;
entries[index].next = buckets[targetBucket];
entries[index].key = key;
entries[index].value = value;
buckets[targetBucket] = index; //单链接的节点数(冲突数)达到了一定的阈值,之后更新散列值
if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer))
{
comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
Resize(entries.Length, true);
}
}
思路分析:
(1)通过哈希函数寻址,计算出哈希地址(因为中间有一个解耦关系buckets,所以不再直接指向entries的索引值,而是buckets的索引)。
(2)判断buckets中映射到的值是否为-1(即为空位)。若不为-1,表示有冲突,遍历冲突链,不允许重复的键。
(3)判断是否有空链表,有则插入空链表的当前位置,将freeList指针后移,freeCount减一,否则将元素插入当前空位。在这一步,容量不足将自动扩容,若当前位置已经存在元素则将该元素的地址存在插入元素的next中,形成一个单链表的形式。类似下图中的索引0。
移除
public bool Remove(TKey key) {
if(key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
} if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
int bucket = hashCode % buckets.Length;
int last = -;//记录上一个节点
//定位到一个单链表,每一个节点都会保存下一个节点的地址,操作不再重新计算哈希地址
for (int i = buckets[bucket]; i >= ; last = i, i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
if (last < ) {
buckets[bucket] = entries[i].next;
}
else {
entries[last].next = entries[i].next;
}
entries[i].hashCode = -;//移除的元素散列值值为-1
entries[i].next = freeList;//将移除的元素放入空列表
entries[i].key = default(TKey);
entries[i].value = default(TValue);
freeList = i;//记录当前地址,以便下个元素能直接插入
freeCount++;//空链表节点数+1
version++;
return true;
}
}
}
return false;
}
Dictionary中存储元素的结构非常有趣,通过一个数据桶buckets将哈希函数与数据数组进行了解耦,使得每一个buckets的值对应的都是一条单链表,在内存空间上却是连续的存储块。同时Dictionary在空间与性能之间做了一些取舍,消耗了空间,提升了性能(影响性能的最大因素是哈希函数)。
移除思路分析:
(1)通过哈希函数确定单链表的位置,然后进行遍历。
(2)该索引对应的值为-1,表示没有没有单链接节点,返回false,结束
(3)该索引对应的值大于-1,表示有单链表节点,进行遍历,对比散列值与key,将映射到的entries节点散列值赋-1,next指向空链表的第一个元素地址(-1为头节点),freeList指向头节点地址,空链表节点数+1,返回true,结束;否则返回false,结束。(此处的节点地址统指索引值)。
查询
public bool TryGetValue(TKey key, out TValue value) {
int i = FindEntry(key);//关键方法
if (i >= ) {
value = entries[i].value;
return true;
}
value = default(TValue);
return false;
} private int FindEntry(TKey key) {
if( key == null) {
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
} if (buckets != null) {
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
for (int i = buckets[hashCode % buckets.Length]; i >= ; i = entries[i].next) {
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
}
}
return -;
}
代码是不是一目了然,在FindEntry方法中,定位单链接的位置,进行遍历,对比散列值与key,比较成功则返回true,结束。
扩容
private void Resize() {
Resize(HashHelpers.ExpandPrime(count), false);
} private void Resize(int newSize, bool forceNewHashCodes) {
Contract.Assert(newSize >= entries.Length); //重新初始化一个比原来空间还要大2倍左右的buckets和Entries,用于接收原来的buckets和Entries的数据
int[] newBuckets = new int[newSize];
for (int i = ; i < newBuckets.Length; i++) newBuckets[i] = -;
Entry[] newEntries = new Entry[newSize]; //数据搬家
Array.Copy(entries, , newEntries, , count); //将散列值刷新,这是在某一个单链表节点数到达一个阈值(100)时触发
if(forceNewHashCodes) {
for (int i = ; i < count; i++) {
if(newEntries[i].hashCode != -) {
newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
}
}
} //单链表数据对齐,无关顺序
for (int i = ; i < count; i++) {
if (newEntries[i].hashCode >= ) {
int bucket = newEntries[i].hashCode % newSize;
newEntries[i].next = newBuckets[bucket];
newBuckets[bucket] = i;
}
}
buckets = newBuckets;
entries = newEntries;
}
foreach遍历
Dictionary实现了IEnumerator接口,是可以用foreach进行遍历的,遍历的集合元素类型为KeyValuePair,是一种键值对的结构,实现是很简单的,包含了最基本的键属性和值属性,
从代码中可以看出,用foreach遍历Dictionary就像用for遍历一个基础数组一样。
这是内部类Enumerator(遍历就是对它进行的操作)中的方法MoveNext(实现IEnumerator接口的MoveNext方法)。
public bool MoveNext() {
if (version != dictionary.version) {
ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
} while ((uint)index < (uint)dictionary.count) {
if (dictionary.entries[index].hashCode >= ) {
current = new KeyValuePair<TKey, TValue>(dictionary.entries[index].key, dictionary.entries[index].value);
index++;
return true;
}
index++;
} index = dictionary.count + ;
current = new KeyValuePair<TKey, TValue>();
return false;
} public struct KeyValuePair<TKey, TValue> {
private TKey key;
private TValue value; public KeyValuePair(TKey key, TValue value) {
this.key = key;
this.value = value;
} //键属性
public TKey Key {
get { return key; }
} //值属性
public TValue Value {
get { return value; }
} public override string ToString() {
StringBuilder s = StringBuilderCache.Acquire();
s.Append('[');
if( Key != null) {
s.Append(Key.ToString());
}
s.Append(", ");
if( Value != null) {
s.Append(Value.ToString());
}
s.Append(']');
return StringBuilderCache.GetStringAndRelease(s);
}
}
Dictionary内部实现结构比Hashtable复杂,因为具有单链表的特性,效率也比Hashtable高。
举例说明:
一,实例化一个Dictionary, Dictionary<string,string> dic=new Dictionary<string,string>();
a,调用Dictionary默认无参构造函数。
b,初始化Dictionary内部数组容器:buckets int[]和entries<T,V>[],分别分配长度3。(内部有一个素数数组:3,7,11,17....如图:);
二,向dic添加一个值,dic.add("a","abc");
a,将bucket数组和entries数组扩容3个长度。
b,计算"a"的哈希值,
c,然后与bucket数组长度(3)进行取模计算,假如结果为:2
d,因为a是第一次写入,则自动将a的值赋值到entriys[0]的key,同理将"abc"赋值给entriys[0].value,将上面b步骤的哈希值赋值给entriys[0].hashCode,
entriys[0].next 赋值为-1,hashCode赋值b步骤计算出来的哈希值。
e,在bucket[2]存储0。
三,通过key获取对应的value, var v=dic["a"];
a, 先计算"a"的哈希值,假如结果为2,
b,根据上一步骤结果,找到buckets数组索引为2上的值,假如该值为0.
c, 找到到entriys数组上索引为0的key,
1),如果该key值和输入的的“a”字符相同,则对应的value值就是需要查找的值。
2) ,如果该key值和输入的"a"字符不相同,说明发生了碰撞,这时获取对应的next值,根据next值定位buckets数组(buckets[next]),然后获取对应buckets上存储的值在定位到entriys数组上,......,一直到找到为止。
3),如果该key值和输入的"a"字符不相同并且对应的next值为-1,则说明Dictionary不包含字符“a”。
基本成员
C# Dictionary源码剖析的更多相关文章
- C# Dictionary源码剖析---哈希处理冲突的方法有:开放定址法、再哈希法、链地址法、建立一个公共溢出区等
C# Dictionary源码剖析 参考:https://blog.csdn.net/exiaojiu/article/details/51252515 http://www.cnblogs.com/ ...
- 转:【Java集合源码剖析】Hashtable源码剖析
转载请注明出处:http://blog.csdn.net/ns_code/article/details/36191279 Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元 ...
- Django Rest Framework源码剖析(六)-----序列化(serializers)
一.简介 django rest framework 中的序列化组件,可以说是其核心组件,也是我们平时使用最多的组件,它不仅仅有序列化功能,更提供了数据验证的功能(与django中的form类似). ...
- 【Java集合源码剖析】Hashtable源码剖析
转载出处:http://blog.csdn.net/ns_code/article/details/36191279 Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元素是一 ...
- Web API 源码剖析之默认配置(HttpConfiguration)
Web API 源码剖析之默认配置(HttpConfiguration) 我们在上一节讲述了全局配置和初始化.本节我们将就全局配置的Configuration只读属性进行展开,她是一个类型为HttpC ...
- Hashtable源码剖析
Hashtable简介 Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. Hashtabl ...
- fasttext源码剖析
fasttext源码剖析 目的:记录结合多方资料以及个人理解的剖析代码: https://heleifz.github.io/14732610572844.html http://www.cnbl ...
- Python源码剖析——02虚拟机
<Python源码剖析>笔记 第七章:编译结果 1.大概过程 运行一个Python程序会经历以下几个步骤: 由解释器对源文件(.py)进行编译,得到字节码(.pyc文件) 然后由虚拟机按照 ...
- jQuery之Deferred源码剖析
一.前言 大约在夏季,我们谈过ES6的Promise(详见here),其实在ES6前jQuery早就有了Promise,也就是我们所知道的Deferred对象,宗旨当然也和ES6的Promise一样, ...
随机推荐
- tp5.0 composer命令插件
1.单元测试composer require topthink/think-testing 1.* (5.0) composer require topthink/think-testing 5.1官 ...
- ubuntu , 安装包的具体文件的查看方法
To see all the files the package installed onto your system, do this: dpkg-query -L <package_name ...
- 3-13《元编程》第5章Class Definitions 3-14(5-4Singleton Classes,2小时)3-15(3小时✅)
类宏 环绕别名 singleton class Class就是增强的模块,类定义也就是模块定义. 5.1 Class Definitions Demystified 5.11 Inside Class ...
- robot framework学习笔记1之_环境安装(win7)
一.简介 Robotframework是基于Python的自动化测试框架.使用关键字驱动的测试方法,自带丰富的库函数可直接引用,可使用Java/Python进行功能库扩展,测试用例使用TSV/HTML ...
- Python基础--列表、元组
一.什么是列表.元组 序列是Python中最基本的数据结构.序列中的每个元素都分配一个数字 - 它的位置,或索引,第一个索引是0,第二个索引是1,依此类推. Python有6个序列的内置类型,但最常见 ...
- 快速读入fread
struct FastIO { static const int S = 1e7; int wpos; char wbuf[S]; FastIO() : wpos(0) {} inline int x ...
- LICEcap 和 FS Capture入门教程
上一篇介绍了如何使用 Visio 图形图表工具,文中贴了一张gif图,留言的小伙伴们迫不及待想知道如何录制 GIF 图,强哥姑且卖弄一次,把 PC 端截图工具和教程分享给大家,分别为 LICEcap ...
- Oracle11g温习-第六章:控制文件
2013年4月27日 星期六 10:33 .控制文件的功能和特点 1) [定义数据库当前物理状态] 2) [维护数据的一致性] 如果控制文件中的检查点与数据文件中的一致,则说明数据一致,可以启动到 ...
- 去掉“Windows文件保护”
1.在“开始→运行”对话框中键入“gpedit.msc”,打开“本地计算机策略→计算机配置→管理模板→系统”窗口,找到“Windows文件保护”组,在右侧窗格中双击“设置Windows文件保护扫描”项 ...
- ESET NOD32 Antivirus – 免费 3个月/ 3PC
ESET NOD32 Antivirus 3个月/ 3PC俄罗斯活动,3用户3个月免费,仅用于EAV,不能用于ESS活动地址: 点此进入申请方法:一共2封邮件,第2封含3个月许可