当前位置:网站首页>C# 基础之字典——Dictionary(二)
C# 基础之字典——Dictionary(二)
2022-08-11 05:31:00 【canon_卡农】
接上回: C# 基础之字典——Dictionary(一)
上次我们知道字典是通过一个数组来存储Entry,那么当我们往字典添加数据的时候,如果数组的容量不够了怎么办呢?
下面我们带着这个疑问来看看底层是怎么实现数组扩容的。
字典是如何扩容的
先看Insert接口内的一段代码:
int index;
if (freeCount > 0) {
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;
可以看到当插入位置count等于容量数组的长度时,则通过Resize()进行扩容,重新定位targetBucket。
Resize()内部则调用 HashHelpers.ExpandPrime:
public static int ExpandPrime(int oldSize)
{
int newSize = 2 * oldSize;
// Allow the hashtables to grow to maximum possible size (~2G elements) before encoutering capacity overflow.
// Note that this check works even when _items.Length overflowed thanks to the (uint) cast
if ((uint)newSize > MaxPrimeArrayLength && MaxPrimeArrayLength > oldSize)
{
Contract.Assert( MaxPrimeArrayLength == GetPrime(MaxPrimeArrayLength), "Invalid MaxPrimeArrayLength");
return MaxPrimeArrayLength;
}
return GetPrime(newSize);
}
最终通过调用 GetPrime 来得到真正的新数组的大小,返回一个约为oldSize两倍大小的素数。源码中定义了一个素数数组:
public static readonly int[] primes = {
3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369};
在源码中可以看到,如果没有指定字典的初始容量,那么会调用Initialize(0),内部将容量初始化为primes数组中最小素数 3.
if (buckets == null) Initialize(0);
这样看来,容量数组大小总为素数,这是为什么呢?
为什么字典的桶buckets 长度为素数?
容量不为素数会出现什么情况
我们假设有这样的一系列keys,他们的分布范围是K={ 0, 1,…, 100 },又假设某一个buckets的长度m=12,因为3是12的一个因子,当key是3的倍数时,那么targetBucket = key%m 也将会是3的倍数.例如:
Keys {0,12,24,36,…} TargetBucket将会是0.
Keys {3,15,27,39,…} TargetBucket将会是3.
Keys {6,18,30,42,…} TargetBucket将会是6.
Keys {9,21,33,45,…} TargetBucket将会是9.
如果Key的值是均匀分布的(每一个Key出现的可能性相同),那么Buckets的Length就没有那么重要了,但是如果Key不是均匀分布呢?
想象一下,如果Key在3的倍数时出现的可能性特别大,其他的基本不出现,TargetBucket那些不是3的倍数的索引就基本不会存储什么数据了,这样就可能有2/3的Bucket空着,数据大量第聚集在0,3,6,9中.
为了减少哈希冲突
这种情况其实是很常见的。
例如,有一种场景,您根据对象存储在内存中的位置来跟踪对象,如果你的计算机的字节大小是4,而且你的Buckets的长度也为4。
那么所有的内存地址都会是4的倍数,也就是说key都是4的倍数,它的HashCode也将会是4的倍数。
导致所有的数据都会存储在TargetBucket=0(Key%4=0)的bucket中,而剩下的3/4的Buckets都是空的. 这样数据分布就非常不均匀了。
每一个key如果与Buckets的长度m有公因子,那么该数据就会存储在这个公因子的倍数为索引的bucket中。
为了让数据尽可能地均匀地分布在Buckets中,我们要尽量减少m和key有公因子出现的可能性。那么,把Bucket的长度设为质数就是最佳选择了,因为质数的因子是最少的。
这就是为什么每次利用Resize给字典扩容时会取大于当前size的最小质数的原因。
避免频繁扩容
在源码中可以看到,每次扩容都会新建数组:
// Resize()
int[] newBuckets = new int[newSize];
for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1;
Entry[] newEntries = new Entry[newSize];
老数组中的内容被拷贝到新数组,然后等待垃圾回收。如果频繁扩容的话,就会产生许多垃圾碎片。
所以在声明字典的时候,应该根据需要存储的数据量指定适当的容量,可以在一定程度上提高性能。
总结:到这里我们已经基本了解了 Dictionary 的内部构造和运作机制。他是由数组构成,并且由哈希函数完成地址构建,由拉链法冲突解决方式来解决冲突。new 时尽量确定大致数量会更加高效。
从内存操作上看,大小以3->7->17->37->….的速度,每次增加2倍多的容量,删除时,并不缩减内存。
边栏推荐
猜你喜欢
【LeetCode-202】快乐数
【LeetCode-74】搜索二维矩阵
5月leetcode-C#刷题日志(持续更新中)
【LeetCode-56】合并区间
Interpretation of the paper: GAN and detection network multi-task/SOD-MTGAN: Small Object Detection via Multi-Task Generative Adversarial Network
Day 87
ARM assembly instruction ADR and LDR
Tinker接入全流程---配置篇
USB URB
buuctf hacknote
随机推荐
微信小程序启动页的实现
swagger常用注释API @ApiModel、@ApiModelProperty的用法
Use the adb command to manage applications
The role of the port
JS事件循环机制
Intelligent risk control China design and fall to the ground
Tinker接入全流程---配置篇
【LeetCode-414】第三大的数
OpenMLDB Pulsar Connector: Efficiently connect real-time data to feature engineering
C语言-6月10日-my_strcpy函数的编写
Certificate of SearchGuard configuration
【LeetCode-73】矩阵置零
OpenMLDB: Consistent production-level feature computing platform online and offline
The whole process of Tinker access --- Compilation
Day 71
ARM assembly instruction ADR and LDR
Day 84
Fourth Paradigm OpenMLDB optimization innovation paper was accepted by VLDB, the top international database association
自己动手写RISC-V的C编译器-02语法描述方法和递归下降解析
欧拉法解微分方程