[C#进阶系列]专题二:你知道Dictionary查找速度为什么快吗?
一、前言
在之前有一次面试中,被问到你了解Dictionary的内部实现机制吗?当时只是简单的了问答了:Dictionary的内部结构是哈希表,从而可以快速进行查找。但是对于更深一步了解就不清楚了。所以面试回来之后,就打算好好研究下Dictionary的源码。所以也就有了这篇文章。
二、Dictionary源码剖析
大家都知道,现在微软已经开源了.NET Framework的源码了,在线源码查看地址为:http://referencesource.microsoft.com/。通过查找可以找到.NET Framework类的源码。下面我们就一起来看下Dictionary源码。
2.1 添加元素
首先我们来查看下Dictionary.Add方法的实现。为了让大家更好地实现,下面抽取了Dictionary源码核心部分来进行分析,详细的分析代码如下所示:
// buckets是哈希表,用来存放Key的Hash值
// entries用来存放元素列表
// count是元素数量
private void Insert(TKey key, TValue value, bool add)
{
if (key == null)
{
throw new ArgumentNullException(key.ToString());
}
// 首先分配buckets和entries的空间
if (buckets == null) Initialize();
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF; // 计算key值对应的哈希值(HashCode)
int targetBucket = hashCode % buckets.Length; // 对哈希值求余,获得需要对哈希表进行赋值的位置 #if FEATURE_RANDOMIZED_STRING_HASHING
int collisionCount = ;
#endif
// 处理冲突的处理逻辑
for (int i = buckets[targetBucket]; i >= ; i = entries[i].next)
{
if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key))
{
if (add)
{
throw new ArgumentNullException();
}
entries[i].value = value;
version++;
return;
} #if FEATURE_RANDOMIZED_STRING_HASHING
collisionCount++;
#endif
} int index; // index记录了元素在元素列表中的位置
if (freeCount > )
{
index = freeList;
freeList = entries[index].next;
freeCount--;
}
else
{
// 如果哈希表存放哈希值已满,则重新从primers数组中取出值来作为哈希表新的大小
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;
version++; #if FEATURE_RANDOMIZED_STRING_HASHING
if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer))
{
comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
Resize(entries.Length, true);
}
#endif
}
下面以一个实际的添加例子来具体分析下上面的添加元素代码,从而更好地理解Add方法的实现原理。
Dictionary<int, string> myDictionary = new Dictionary<int, string>();
myDictionary.Add(, "Item 1");
myDictionary.Add(, "Item 2");
myDictionary.Add(, "Item 3");
当添加第一个元素时,此时会分配哈希表buckets数组和entries数组的空间和初始大小为3,分配完成之后,会计算添加元素key值的哈希值,哈希值的计算由具体的哈希算法来实现的,假设1的哈希值为9的话,此时targetBucket = 9%buckets.Length(3)的值为0,index的值为0,则第一个元素存放在entries列表中的第一个位置,最后对哈希表进行赋值,此时赋值的位置为第0个位置,其值为index的值,所以为0,插入第一个元素后Dictionary的内部结构如下所示:
后面添加元素的过程依次类推。其原理就是,buckets记录了元素的在元素列表的存储位置,也就相当于一个映射列表。在查找的时候,就可以通过key值的哈希值来与buckets数组长度求余来获得元素在元素列表中的索引,这样就可以快速定位元素的位置,从而获得元素的key对应的Value值。如上面的例子中,如果想找到key值为1对应的Value值时,此时计算1的哈希值为9,然后对buckets数组长度求余,此时获得的值正是0,这样就可以直接从entries[0].Value的方式来获取对应的Value的值,这也就是Dictionary能完成快速查找的实现原理。后面会通过Dictionary内部的查找源码来证实上面分析的过程。
2.2 解决冲突
在添加元素过程中,有一个很重要的问题,如果产生冲突怎么办?即如果我后面需要插入的一个元素(假设这个值为11吧)的key值的哈希值也为6,此时targetBucket的值也是为0,但元素列表中0的位置已经存放了元素了,这样就出现了冲突,那Dictionary是怎样处理这个冲突的呢?处理冲突的方法有很多种,Dictionary处理的方式是链接法。Dictionary会把发生冲突的元素链接之前元素的后面,通过next属性来指定冲突关系。此时Dictionary内部结构如下图所示:
三、Dictionary如何实现快速查找呢?
针对于Dictionary实现快速查找的原因,在上面我们已经做了一个推断了,下面通过Dictionary内部的代码实现来验证下,具体的查找代码如下所示:
public TValue this[TKey key]
{
get
{
int i = FindEntry(key);
// 通过元素所在存在的位置直接获取其对应的Value
if (i >= ) return entries[i].value;
throw new KeyNotFoundException();
return default(TValue);
}
set
{
Insert(key, value, false);
}
} private int FindEntry(TKey key)
{
if (key == null)
{
throw new ArgumentNullException();
} if (buckets != null)
{
// 获得Key值对应的哈希值
int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
// 查找元素在元素列表中的位置,如果没有冲突的情况下,此时查找速度为O(1),存在冲突的情况下为O(N),N为存在冲突的次数
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 -;
}
通过代码可以看出,我们之前的分析是完成正确的。从中可以明白:Dictionary之所以能实现快速查找元素,其内部使用哈希表来存储元素对应的位置,然后我们可以通过哈希值快速地从哈希表中定位元素所在的位置索引,从而快速获取到key对应的Value值。
四、总结
可以说,Dictionary的实现原理也是一种空间换时间的思路,多使用一个buckets的存储空间来存储元素的位置,从而来提升查找速度。
接下来,我们新开一个领域驱动设计系列,还请大家多多拍砖。
本文所有源码下载:DictonaryInDepth.zip
[C#进阶系列]专题二:你知道Dictionary查找速度为什么快吗?的更多相关文章
- 当我们说线程安全时,到底在说什么——Java进阶系列(二)
原创文章,同步发自作者个人博客,转载请以超链接形式在文章开头处注明出处http://www.jasongj.com/java/thread_safe/ 多线程编程中的三个核心概念 原子性 这一点,跟数 ...
- [.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店
一.前言 在前面专题一中,我已经介绍了我写这系列文章的初衷了.由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这 ...
- [你必须知道的NOSQL系列]专题二:Redis快速入门
一.前言 在前一篇博文介绍了MongoDB基本操作,本来打算这篇博文继续介绍MongoDB的相关内容的,例如索引,主从备份等内容的,但是发现这些内容都可以通过官方文档都可以看到,并且都非常详细,所以这 ...
- 方法字段[C# 基础知识系列]专题二:委托的本质论
首先声明,我是一个菜鸟.一下文章中出现技术误导情况盖不负责 引言: 上一个专题已和大家分享了我懂得的——C#中为什么须要委托,专题中简略介绍了下委托是什么以及委托简略的应用的,在这个专题中将对委托做进 ...
- [C# 基础知识系列]专题二:委托的本质论 (转载)
引言: 上一个专题已经和大家分享了我理解的——C#中为什么需要委托,专题中简单介绍了下委托是什么以及委托简单的应用的,在这个专题中将对委托做进一步的介绍的,本专题主要对委本质和委托链进行讨论. 一.委 ...
- [C#进阶系列]专题一:深入解析深拷贝和浅拷贝
一.前言 这个星期参加了一个面试,面试中问到深浅拷贝的区别,然后我就简单了讲述了它们的之间的区别,然后面试官又继续问,如何实现一个深拷贝呢?当时只回答回答了一种方式,就是使用反射,然后面试官提示还可以 ...
- [C#]网络编程系列专题二:HTTP协议详解
转自:http://www.cnblogs.com/zhili/archive/2012/08/18/2634475.html 我们在用Asp.net技术开发Web应用程序后,当用户在浏览器输入一个网 ...
- javascript进阶系列专题:闭包(Closure)
在javascript中,函数可看作是一种数据,可以赋值给变量,可以嵌套在另一个函数中. var fun = function(){ console.log("平底斜"); } f ...
- javascript进阶系列专题:作用域与作用域链
字面意思,作用域是指变量和函数的作用范围,换言之,作用域决定了变量和函数的可见性和有效时间.javascript作用域是用函数来区分,与其他语言的大括号不同. for (var i=0; i<5 ...
随机推荐
- .Net判断一个对象是否为数值类型
这乍一听是个很简单的事,但突然搞起来还真有点无从下手的感觉. 首先当然是通过GetType()方法反射获取其类型信息,然后对其进行分析,但是类型信息Type中并没有简单地给出这么一个属性进行判断. 老 ...
- Only the sqlmigrate and sqlflush commands can be used when an app has migrations.
samcao@samcao-Lenovo-IdeaPad-Y470:~/caodjango/caossh$ python manage.py sqlall getssh System check id ...
- 2-2. Initializing Objects with Initializer Lists
Using Uniform Initialization to Construct a vector #include <iostream> #include <vector> ...
- 安装CocoaPods碰到的问题
1.安装完Pods后第一次使用pod install命令提示"Setting up CocoaPods master repo" 解决办法: 第一次使用pod命令时,先执行以下po ...
- Spring.net 间接调用被AOP拦截的方法失效(无法进入aop的拦截方法)
.下面的tx要定义 <objects xmlns="http://www.springframework.net" xmlns:db="http://www.spr ...
- 在ASP.NET MVC中使用Juqery实现页面局部刷新
自己做的一个实验,留作备忘,此实例包括扩一下几个文件: 1.MyMovieController.cs 2.Index.aspx 3.ViewUserControl1.ascx 4.movie类 其中M ...
- PHP 实现单一入口 apache配置
在apache的httpd.conf加入,需要把LoadModule rewrite_module modules/mod_rewrite.so前面的“#”去掉 DocumentRoot / < ...
- Zepto 实现checkbox全选与全不选状态切换
最近项目里用到foundation,而foundation4默认集成了Zepto,很多轮子要重造,所以有了下面的代码. <script> /** * Muti-Checking-Toggl ...
- css高度自适应
何为高度自适应? 高度自适应就是高度能跟随浏览器窗口的大小改变而改变,典型的运用在一些后台界面中上面一栏高度固定用作菜单栏或导航栏,下面一栏高度自适应用于显示内容.高度自适应不像宽度自适应那样简单,在 ...
- 文件操作总结 (Path,Directory,File)
Path类就是对字符串的操作,与实际的文件没有任何关系 属性: Path.GetFileName("路径"), //获取文件名带后缀: Path.GetFileNameWithou ...