三分钟理解 C#字典的底层实现

C#字典底层

关于字典的初始化

字典的构造函数 提供了一个可以提前输入字典容量的选项,这个选项可以让字典提前把容量拓建好,而完全不会影响使用!所以为了 节省性能 ,提倡在使用字典的时候 都大概估计一个差不多的容量


论字典是如何实现高效的查找数据的

  • 字典很重要的两个内容是 哈希桶 和 Entry数组
    • 哈希桶 和 Entry数组 容量都是取一个 大于当前给定容量的一个 质数(防止被除尽)
    • 哈希桶用于 “分组” (主要用来进行哈希碰撞),会和对应的Entry单位形成链表,而哈希桶里存着各种链表头
    • Entry单位 用来存储字典的内容(指 key,value),并且标识下一个元素的位置。
  • 查找数据的时候,根据 key 获取一个 hash值,然后根据hash值去 哈希桶 找到对应的链表(即所谓的 进行一次碰撞),去遍历对应的链表,最终获取 value (这种分组式的获取效率是真的快)

Add操作

  • 当加入新元素时,根据 targetBucket = hashCode % buckets.Length; buckets.Length 来得到 新元素 要具体加入哪一个组。如果 targetBucket 未被使用过,则会有一个 启用bool值的更改
    • 这里的 HashCode 是由 Key 计算得到,如果Key是int ,则HashCode直接就是对应的数据,如果是字符or字符串之类的,则会通过一个内置的 GetHashCode 的函数去获得 HashCode
  • 如果 再 加入新元素 计算出来的 targetBucket 已经被启用了,则会出现 哈希碰撞 的情况(指两个元素计算出来的哈希值是相同的)。C#的解决方法是 拉链法 ,即 **让 发生了哈希碰撞的元素 形成一个链表 **(故形象为拉链,即链表)

Remove 操作

  • 字典针对移除操作弄了一个内容叫 FreeList 的列表 来存储 被删除Entry在Entries中的下标index(里面存的下标 代表在 Entry数组中这个下标位置是空闲的)
  • Remove 操作内容为 先查找(哈希碰撞后,遍历链表) 对应的 Entry 然后删除,然后被删除Entry 的下标存入 FreeList
  • 在添加数据时,优先查找 FreeList 里所记录的空闲位置,然后优先向所记录的空位置加数据。若 FreeList 为空,则按照 Count(这里的 Count 是一个用于记录 字典当前存储的有效元素的数量) 去依次加数据
  • 这些操作的目的是 重复利用数组元素被删除后 产生的空隙,使得计算机可以 按批次处理数据 更为高效

Resize扩容

  • 扩容出现的情况
    1. 第一种情况自然就是数组已经满了,没有办法继续存放新的元素
    2. **第二种,Dictionary中发生的碰撞次数太多,会严重影响性能,**也会触发扩容操作。
      • 解释一下,就是 哈希碰撞全撞在这一个 hashCode 上了, 导致同一个 hashCode 下的链表 太长了,遍历起来费时费力,浪费性能,所以会有一个 碰撞的阈值 来保证其性能
  • 扩容操作内容
    • 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的实现原理,其中有两个关键的算法,

  • 一个是Hash算法,
  • 一个是用于应对Hash碰撞冲突解决算法。
-Hash算法

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

上面是比较学院派的说法,按照我自己的说法就是 根据所给的 Key 而加工获取 得到一个 hashCode 的算法。这种算法有以下几种特征

  1. 相同的数据进行Hash运算,得到的结果一定相同。HashFunc(key1) == HashFunc(key1)
    2.不同的数据进行Hash运算,其结果也可能会相同,(Hash会产生碰撞)。key1 != key2 => HashFunc(key1) == HashFunc(key2).
    3.Hash运算时不可逆的,不能由key获取原始的数据。key1 => hashCode但是hashCode ==> key1

Hash算法有很多种,而C#字典用到的是 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映射到具体的哪个桶中。

-Hash碰撞冲突解决算法

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

**1. 拉链法(开散列):**将产生冲突的元素建立一个单链表,并将头指针地址存储至Hash表对应桶的位置。这样定位到Hash表桶的位置后可通过遍历单链表的形式来查找元素。
2. 开放定址法(闭散列):当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
**3. 再Hash法:**顾名思义就是将key使用其它的Hash函数再次Hash,直到找到不冲突的位置为止。

C# 的字典 就是用到了这里的 拉链法


这篇文章只是我自己因为网上的博客讲的黑话太多了,所以为了让自己更好的理解这些内容,用一种“讲人话”的手段去把这些内容再梳理了一遍。倘若需要更进一步的了解其中的知识,建议去看看这位dalao的文章,图文并茂,非常有参考价值!!!

参考的文章

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值