C#字典底层原理

一:前言

Dictionary是一种键值对的形式存放数据,即 key和value一一映射。key的类型没有限制,可以是整数、字符串甚至是实例对象

C#字典源码

时间复杂度

------Add:O(1)

------Remove:一般情况下为O(1),最差情况为O(n)

------Find:一般情况下为O(1),最差情况为O(n)


二:相关介绍

------Hash算法:Hash算法是一种数字摘要算法,将不定长度的数据根据不同的算法变成一个固定长度的Hash值,Hash值具有不可逆性的特点。常见的MD5算法就是一种Hash算法

------Hash函数:也称为散列函数。有很多种Hash函数,最常见的就是除留余数法,用Hash值除以一个值求余计算出一个索引值

------哈希冲突:不同的值通过哈希算法后计算出的哈希值可能相同,那么就产生了哈希冲突,解决哈希冲突的方法有开放定址法、再哈希法、拉链法。C#字典解决哈希冲突的方法是拉链法

------字典内部是通过两个数组存储,如下图所示,一个是哈希桶用于解决哈希冲突,存储的是每个链表的头结点Entry下标。一个是Entry数组,内部形成一个链表,存储每一个数据实体

因为如果只用一个数组存储,一是每个Hash值对应一个索引那么需要声明一个超级大容量的数组,二是两个不同的key生成的哈希值也有可能相同会产生Hash碰撞。于是就有了哈希桶,将Hash值分类装到一个个桶里,减小了索引的范围,也使查找的效率更高

举个列子,如果我们知道一个人的身份证号,想要查询他的信息,拿身份证号去全国的数据库里查找肯定会很慢,但我们知道,从身份证号可以看出这个人是属于哪个省份或地区的,去所在地区的数据库查找,那肯定就比较快了

cs 复制代码
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
}

private int[] buckets;		// 哈希桶数组
private Entry[] entries;	// 数据实体数组
...

三:底层实现

------构造字典

声明字典容量相当于声明哈希桶和Entry数组的容量,两个桶的容量会取大于给定容量的一个质数

容量设置为质数的原因是因为计算桶下标是根据Hash值除以桶长度求余获得,那么Hash值和桶长度的公因子就应该尽量少,如果公因子多,那么数据的分布会不均匀,很多桶会是空的,所以容量设置为质数(素数)就是最佳选择了,

------Add

首先通过内置的GetHashCode函数计算出一个哈希值(如果是数值类型则哈希值就是这个数值,如果是其他类型则会通过某种算法计算出一个哈希值,GetHashCode(key) & 0x7FFFFFFF,逻辑与是为了确保Hash值是一个正整数),然后通过除留余数法计算出桶索引,将当前Entry的next指向上一个头结点Entry的下标,将buckets对应的桶索引设置为当前Entry的下标,相当于每次添加的Entry都是头结点

cs 复制代码
entries[index].hashCode = hashCode;
entries[index].next = buckets[targetBucket];
entries[index].key = key;
entries[index].value = value;
buckets[targetBucket] = index;

------Remove

字典内部声明了三个字段,FreeCount、FreeList和Count

FreeCount用于记录当前有几个被删除的元素位置是空闲的,FreeList用于存储上一个被删除的Entry下标,也是一个单链表结构,当删除某个元素时,首先找到对应的Entry数组位置将其删除,并赋值给FreeList,当下次添加元素时,优先判断FreeCount是否大于0,添加到FreeList空闲位置,如果没有空闲位置,则按照Count字段记录的位置去添加元素,Count表示为字典当前存储的有效元素的数量,每次Add后会+1

------Resize

字典出现的扩容的时机有两种,一个是数组已经满了无法存放新的元素,二是发生哈希碰撞的次数太多了,会影响性能(某一个哈希值的哈希碰撞次数过多导致这个哈希值下的链表太长了,遍历起来费时费力,浪费性能,所以会有一个碰撞的阈值来保证其性能)

扩容的过程首先是申请两倍于现在大小的buckets、entries并取大于给定容量的一个质数,然后将现有的元素拷贝到新的entries中(如果是Hash碰撞太多导致扩容,则使用新HashCode函数重新计算Hash值,再重建Hash链表)

------Find

字典内部查找时候FindEntry方法,首先通过同样的操作找到链表头节点的位置,通过遍历链表,比对hashCode和key值找到目标数据

查找某个key对应的valye时时使用TryGetValue只调用了1次FindEnty,而使用ContainKey判断是否存在后还需要通过this得到vlaue,一共需要调用2次FindEnty


四:几种数据结构的比较

这里比较一下Dictionary、HashSet、Hashtable、List

------如果是通过key去查找,Dictionary和Hashtable的效率是高于List的,如果是通foreash遍历,则List的效率更高(List底层是数组,内存是连续的。Dictionary和Hashtable底层是哈希表,根据Hash算法进行存储内存是不连续的,会产生更多的换页操作)

------HashSet的查找效率要高于List,因为HashSet内部原理是哈希表,只不过与Dictionary和Hashtable不同的是只存了key

------HashSet添加元素时会判断是否已经存在,不会添加重复的元素,可以使用HashSet去重

相关推荐
算法歌者几秒前
[算法]入门1.矩阵转置
算法
林开落L15 分钟前
前缀和算法习题篇(上)
c++·算法·leetcode
远望清一色16 分钟前
基于MATLAB边缘检测博文
开发语言·算法·matlab
tyler_download18 分钟前
手撸 chatgpt 大模型:简述 LLM 的架构,算法和训练流程
算法·chatgpt
SoraLuna38 分钟前
「Mac玩转仓颉内测版7」入门篇7 - Cangjie控制结构(下)
算法·macos·动态规划·cangjie
我狠狠地刷刷刷刷刷41 分钟前
中文分词模拟器
开发语言·python·算法
鸽鸽程序猿41 分钟前
【算法】【优选算法】前缀和(上)
java·算法·前缀和
九圣残炎1 小时前
【从零开始的LeetCode-算法】2559. 统计范围内的元音字符串数
java·算法·leetcode
YSRM1 小时前
Experimental Analysis of Dedicated GPU in Virtual Framework using vGPU 论文分析
算法·gpu算力·vgpu·pci直通
韭菜盖饭1 小时前
LeetCode每日一题3261---统计满足 K 约束的子字符串数量 II
数据结构·算法·leetcode