使用C#已经有好多年头了,然后突然有一天被问到C#Dictionary的基本实现,这让我反思到我一直处于拿来主义,能用就好,根本没有去考虑和学习一些底层架构,想想令人头皮发麻。下面开始学习一些我平时用得理所当然的东西,今天先学习一下字典,Dictionary

一、Dictionary源码学习

Dictionary实现我们主要对照源码来解析,目前对照的源码版本是.Net Framwork4.8,源码地址

这边主要介绍Dictionary中几个比较关键的类和对象,然后跟着代码来走一遍插入、删除和扩容的流程。

1、Entry结构体

首先,我们引入Entry这样一个结构体,它的定义如下面代码所示,这是Dictionary中存放数据的最小单位,调用Add(Key,Value)方法添加的元素都会被封装在这样的一个结构体中。

         private struct Entry
{
public int hashCode; // Lower 31 bits of hash code, -1 if unused
public int next; // Index of next entry, -1 if last
public TKey key; // Key of entry
public TValue value; // Value of entry
}

2、其他关键私有变量

 private int[] buckets; // Hash桶
private Entry[] entries; // Entry数组,存放元素
private int count; // 当前entries的index位置
private int version; // 当前版本,防止迭代过程中集合被更改
private int freeList; // 被删除Entry在entries中的下标index,这个位置是空闲的
private int freeCount; // 有多少个被删除的Entry,有多少个空闲的位置
private IEqualityComparer<TKey> comparer; // 比较器
private KeyCollection keys; // 存放Key的集合
private ValueCollection values; // 存放Value的集合

3、Dictionary的构造

         private void Initialize(int capacity)
{
int prime = HashHelpers.GetPrime(capacity);
this.buckets = new int[prime];
for (int i = ; i < this.buckets.Length; i++)
{
this.buckets[i] = -;
}
this.entries = new Entry<TKey, TValue>[prime];
this.freeList = -;
}

我们看到,Dictionary在构造的时候做了以下几件事:

1、初始化一个this.buchkets=new int[prime]

2、初始化一个this.entries=new Entry<TKey,TValue>[prime]

3、Bucket和entries的容量都为大于字典容量的一个最小的质数

其中this.buckets主要用来进行Hash碰撞,this.entries用来存储字典的内容,并且标识下一个元素的位置。

4、Dictionary——Add操作

我们以Dictionary<int,string>为例,来展示一下Dictinoary如何添加元素:

首先,我们构造一个,容量为6:

Dictionary<int, string> test = new Dictionary<int, string>();

Test.Add(,"")

根据Hash算法:4.GetHashCode()%7=4,因此碰撞到buckets中下表为4的槽上,此时由于Count为0,因此元素放在Entries中第0个元素上,添加后,Count变为1

Test.Add(,"")

根据Hash算法,11.GetHashCode()%7=4,因此再次碰撞到Buckets中下标为4的槽上,由于此槽上的值已经不是-1,此时Count=1,因此把这个新加的元素放到entries中下标为1的数组中,并且让Buckets槽指向下表为1的entries中,下标为1的entry之下下表为0的entries。

Test.Add(,"")
Test.Add(,"")

5、Dictionary——Remove操作

Test.Remove()

我们删除元素时,通过一次碰撞,并且沿着链表寻找3次,找到key为4的元素所在的位置,删除当前元素,并且把FreeList的位置指向当前删除元素的位置,FreeCount置为1。

删除的数据会形成一个FreeList的链表,添加数据的时候,优先向FreeList链表中添加数据,FreeList为空则按照count一次排序。

6、Dictionary——Resize操作(扩容)

有细心的小伙伴可能看过Add操作后就想问了,buckets、entries不就是两个数组么,那万一数组放满了怎么办?接下来就是我要介绍的Resize(扩容)这样一种操作,对我们的buckets、entries进行扩容。

6.1 扩容操作的触发条件

首先我们需要直到在什么情况下,会发生扩容操作:

第一种情况自然就是数组已经满了,没有办法继续存放新的元素,如下图所示。

第二种,Dictionary中发生的碰撞次数太多,会严重影响性能,也会出发扩容操作。

Hash运算会不可避免的产生冲突,Dictionary中使用拉链发来解决冲突的问题,但是,大家看下图中的这种情况,所有的元素都刚好落在buckets[3]上面,结果就导致了时间复杂度O(n),查找性能会下降:

 6.2 扩容操作如何进行

为了给大家演示清楚,模拟了以下这种数据结构,大小为2的Dictionary,假设碰撞的阈值为2;现在出发Hash碰撞扩容。

1、申请两倍于现在大小的buckets、entries

2、将现有的元素拷贝到新的entries

3、如果时Hash碰撞扩容,使用新HashCode函数重新计算Hash值

4、对entries每个元素bucket=newEntries[i].hashCode%newSize确定新buckets位置

5、重建hash链,newEntries[i].next=buckets[bucket];buckets[bucket]=i;

关注点

对于Dictionary的实现原理,其中有两个关键的算法,1、Hash算法。2、用于对应Hash碰撞冲突解决算法。

二、Hash算法

Hash算法是一种术宇摘要算法,它将能不定长度的二进制数据集给映射到一个较短的二进制长度数据集。

实现了Hash算法的函数我们叫它Hash函数,Hash函数有以下几点特征。

1、相同的数据进行Hash运算,得到的结果一定是相同的,HashFunc(key1)==HashFunc(key1)

2、不同的数据进行Hash运算,其结果也可能会相同,(Hash会产生碰撞)。key1!=key2=>HashFunc(key1)==HashFunc(key2)。

3、Hash运算是不可逆的,不能由key获取原始的数据,key1=>hashCode但是hashCode==>key1

关于Hash碰撞下图很清晰的就解释了,可从图中得知Sandra Dee 和 John Smith 通过hash运算后,落到了02位置,产生了碰撞和冲突。

常见的构造Hash函数的算法有以下几种。

1、直接寻址法:取keyword或者keyword的某个线性函数值为散列地址,即H(key)=key或者H(key)=a·key+b,当中a和b为常数(这样的散列函数叫做自身函数)。这个的应用就是,比如我们世界地图的掩码,直接用坐标x*1000+坐标y,得到key。

2、数字分析法:找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。分析一组数据,比方一组员工的出生年月日,这时,我们发现出生年月日的前几位数字大体相同,这种话,出现冲突的几率就会非常大,可是我们发现年月日的后几位表示月份和详细日期的数字区别非常大,假设用后面的数字来构造散列地址,则冲突几率就会明显减少。

3、平方取中法:取keyword平方后的中间几位作为散列地址。

4、折叠法:将keyword切割成位数同样的几部分,最后一部分分数能够不同,然后取这及部分的叠加和(去除进位)作为散列地址。

5、随机数法:选择一随机函数,取keyword的随机值作为散列地址,通常适用于keyword长度不同的场合。

6、除留余数法:取keyword被某个不大于散列表表长m的数p除后所得的余数为散列地址。即H(key)=key MOP p ,  p<=m。不仅能够对keyword直接取模,也可在折叠、平方取中等运算后取模,对p的选择非常重要,一般取素数或m,若p选的不好,容易产生碰撞。

三、Hash桶算法

说到Hash算法大家就会想到Hash表,一个Key通过Hash函数运算后可快速的得到hashCode,通过hashCode的映射可以直接Get到Value。但是hashCode一般取值都是非常大的。经常是2^32以上,不可能对每个hashCode都指定一个映射。因为这样的一个问题,所以人们就将生成的HashCode以分段的形式来映射,把每一段称之为一个Bucket(桶),一般常见的Hash桶就是直接对结果取余。

假设将生成的hashCode可能取值有2&32个,然后将其切分成一段一段,使用8个桶来映射,那么就可以通过bucketIndex=HashFunc(key1)%8 这样一个算法来确定这个hashCode映射到具体哪个桶中。

Dictionary就是这用的哈希桶算法。

int hashCode =comparer.GetHashCode(key)&0x7FFFFFFF;
int targetBucket = hashCode %buckets.Length;

四、Hash碰撞冲突解决算法

对于一个hash算法,不可避免地会产生冲突,那么产生冲突以后如何处理,是一个很关键的地方,目前常见的冲突解决算法有拉链法(Dictionary实现采用的)、开放定址法、再Hash法、公共溢出分区法。

1、拉链法(开散列):将产生冲突的元素建立一个单链表,并将头指针地址存储之Hash表对应桶的位置,这样定位到Hash表桶的位置后通过遍历单链表的形式来查找元素。

2、开放定址法(闭散列):当发生哈希冲突时,如果哈希表未被装满,说明再哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个”空位置中去。

3、再Hash法:顾名思义就是将key使用其他的Hash函数再次Hash,直到找到不冲突的位置为止。

拉链法:

开放地址法:

假设现在有一个关键码集合(1、4、5、6、7、9),哈希结构的容量为10,哈希函数为Hash(key)=key%10。将所有关键码插入到该哈希结构中,如图:

假如仙子啊有一个关键码24要插入该结构中,使用哈希函数求得哈希地址为24,但是该地址已经存放了元素,此时发生哈希冲突。

线性探测:从发生哈希冲突的位置开始,一次向后探测,直到找到下一个空位置为止,例如上面的地址,插入关键码24时,进行线性探测,插入后如下图:

限制:

1、用该方法需要关键码必须为整形才能被模,所以我们需要实现将非整形转化为整形。

2、模的数值最好为素数,需要我们创建一个素数表。

3、增容问题。

好了,关于Dictionary的相关知识,就先介绍到这里了。

浅谈C# Dictionary实现原理的更多相关文章

  1. TODO:浅谈pm2基本工作原理

    TODO:浅谈pm2基本工作原理 要谈Node.js pm2的工作原理,需要先来了解撒旦(Satan)和上帝(God)的关系. 撒旦(Satan),主要指<圣经>中的堕天使(也称堕天使撒旦 ...

  2. 浅谈SpringBoot核心注解原理

    SpringBoot核心注解原理 今天跟大家来探讨下SpringBoot的核心注解@SpringBootApplication以及run方法,理解下springBoot为什么不需要XML,达到零配置 ...

  3. 浅谈springboot自动配置原理

    前言 springboot自动配置关键在于@SpringBootApplication注解,启动类之所以作为项目启动的入口,也是因为该注解,下面浅谈下这个注解的作用和实现原理 @SpringBootA ...

  4. 浅谈 session 会话的原理

    先谈 cookie 网络传输基于的Http协议,是无状态的协议,即每次连接断开后再去连接,服务器是无法判断此次连接的客户端是谁. 如果每次数据传输都需要进行连接和断开,那造成的开销是很巨大的. 为了解 ...

  5. 浅谈JavaScript DDOS 攻击原理与防御

    前言 DDoS(又名"分布式拒绝服务")攻击历史由来已久,但却被黑客广泛应用.我们可以这样定义典型的DDoS攻击:攻击者指使大量主机向服务器发送数据,直到超出处理能力进而无暇处理正 ...

  6. 浅谈HashMap 的底层原理

    本文整理自漫画:什么是HashMap? -小灰的文章 .已获得作者授权. HashMap 是一个用于存储Key-Value 键值对的集合,每一个键值对也叫做Entry.这些个Entry 分散存储在一个 ...

  7. JAVA容器-浅谈HashMap的实现原理

    概述 HashMap是通过数组+链表的方式实现的,由于HashMap的链表也是采用数组方式,我就修改直接利用LinkedList实现,简单模拟一下. 1.Key.Value的存取方式. 2.HashM ...

  8. JAVA NIO之浅谈内存映射文件原理与DirectMemory

    JAVA类库中的NIO包相对于IO 包来说有一个新功能是内存映射文件,日常编程中并不是经常用到,但是在处理大文件时是比较理想的提高效率的手段.本文我主要想结合操作系统中(OS)相关方面的知识介绍一下原 ...

  9. 浅谈拒绝服务攻击的原理与防御(4):新型DDOS攻击 – Websocket和临时透镜

    0×01 前言 前几天我已经分别发了三篇关于DDOS攻击相关的文章,我也是第一次在freebuf上发表这种文章,没想到有那么多人点击我真的很开心,前几天我为大家介绍的DDOS攻击的方法和原理都是已经出 ...

随机推荐

  1. Convert between Unix and Windows text files - IU Knowledge Base from: https://kb.iu.edu/d/acux

    vi. To input the ^M character, press Ctrl-v , and then press Enter or return . In vim, use :set ff=u ...

  2. 2019-2020-1 20199324《Linux内核原理与分析》第七周作业

    第六章 进程的描述和进程的创建 知识点总结 进程的描述 操作系统内核实现操作系统的三大管理功能以及对应的抽象概念: 进程管理(最核心)-- 进程 内存管理 -- 虚拟内存 文件系统 -- 文件 进程是 ...

  3. [Algo] 281. Remove Spaces

    Given a string, remove all leading/trailing/duplicated empty spaces. Assumptions: The given string i ...

  4. 一个http的Post请求问题,unable to resolve host <我的域名>:no address associated with hostnam

    原因:你应用中写入的测试服务器地址baseURL解析不了,服务器端设置的原因: 解决:找服务端修改设置,或者Android应用中把测试地址改为上线服务器地址.

  5. Zblog主题模板自适应手机响应式ZblogPHP简洁博客主题

    Z-blog PHP版本简洁主题模板 特点简洁舒适 手机移动端自适应,完美有利于优化 代码结构利于编辑 对于不懂代码的,也非常适合简答后台简答 PC端侧边栏下拉跟随,无论下面有多长,导航侧边栏都只在左 ...

  6. python学习笔记(17)urllib.parse模块使用

    url.parse :定义了url的标准接口,实现url的各种抽取 parse模块的使用:url的解析,合并,编码,解码 使用时需导入 from urllib import parse urlpars ...

  7. iOS运营级B2B服务平台App、自定义图标库、个人中心页面、识别身份证Demo、瀑布流等源码

    iOS精选源码 简单的个人中心页面-自定义导航栏并予以渐变动画 一个近乎完整的可识别中国身份证信息的Demo 可自动快速... iOS可自定义图表库 - PNChart 开源一款曾是运营级的B2B服务 ...

  8. [LC] 572. Subtree of Another Tree

    Given two non-empty binary trees s and t, check whether tree t has exactly the same structure and no ...

  9. .vimrc文件

    1 set number 2 set shiftwidth=4 3 set softtabstop=4 4 set tabstop=4 5 set expandtab 6 "set hlse ...

  10. 网页title滚动

    );        var leftstar=title.substring (1,title.length );        document.title =leftstar +firstch ; ...