Dictionary<TKey,TValue>底层原理

深入解析C#Dictionary的底层原理

目录

一、Dictionary介绍:

二、基本构成

三、核心方法

一.Add操作

解决冲突算法:

二.TryGetValue等查找元素方法

三.Resize()----扩容

四、总结


前言:

        分享一个面试的高频问题,就是C#一些集合的底层原理,今天先说一下Dictionary,这里是我在看了源码之后的一些理解和总结,欢迎大家积极讨论,和批评指正

一、Dictionary介绍:

Dictionary<TKey,TValue>的查询数据所花费的时间是所有集合类里面最快的,因为内部使用了散列函数加双数组来实现,所以其查询数据的时间复杂度为 O(1)。Dictionary<TKey,TValue>的实现是一种典型的牺牲空间换取时间的做法

二、基本构成

 

首先是最核心的部分 这个struct相当于一个链表结点,int 类型的 next指向entries数组中下一个Entry的索引

private struct Entry {
	public int hashCode;    // 降低31位哈希码,如果不使用则为-1
	public int next;        // 下一项的索引,如果是最后一项则为-1
	public TKey key;         
	public TValue value;    
}

其次是我们口中常说的双数组:

Dictionary<Tkey,TValue>内部有两个数组,一个名为buckets,用于存放由多个同义词组成的静态链表的头指针(链表的第一个元素在数组中的索引,当它值为-1时表示此哈希地址不存在元素);另一个数组为entries,存放的是Entry结构体,同时这些数据通过next指针构成单链表。

        private int[] buckets;
        private Entry[] entries;
        private int count;
        private int version;
        private int freeList;
        private int freeCount;
        private IEqualityComparer<TKey> comparer;
        private KeyCollection keys;
        private ValueCollection values;
        private Object _syncRoot;
        
        // constants for serialization
        private const String VersionName = "Version";
        private const String HashSizeName = "HashSize";  // Must save buckets.Length
        private const String KeyValuePairsName = "KeyValuePairs";
        private const String ComparerName = "Comparer";

 三、核心方法

一.Add操作

源码中Add(key,value)直接调用Insert(key,value,true),如下是Insert方法

private void Insert(TKey key, TValue value, bool add) {

	if( key == null ) {
				ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
			}

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

	#if FEATURE_RANDOMIZED_STRING_HASHING
	int collisionCount = 0;
	#endif

	for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
				if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
					if (add) { 
						ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
					}
					entries[i].value = value;
					version++;
					return;
				} 

				#if FEATURE_RANDOMIZED_STRING_HASHING
	collisionCount++;
				#endif
				}
	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;
	version++;

	#if FEATURE_RANDOMIZED_STRING_HASHING

	#if FEATURE_CORECLR
	// In case we hit the collision threshold we'll need to switch to the comparer which is using randomized string hashing
	// in this case will be EqualityComparer<string>.Default.
	// Note, randomized string hashing is turned on by default on coreclr so EqualityComparer<string>.Default will 
	// be using randomized string hashing

	if (collisionCount > HashHelpers.HashCollisionThreshold && comparer == NonRandomizedStringEqualityComparer.Default) 
			{
				comparer = (IEqualityComparer<TKey>) EqualityComparer<string>.Default;
				Resize(entries.Length, true);
			}
	#else
	if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) 
			{
				comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
				Resize(entries.Length, true);
			}
	#endif // FEATURE_CORECLR

	#endif

	}

解决冲突算法:

Dictionary<TKey,TValue>计算Key的哈希值使用的是取余法,解决冲突的方法是链接法

我们可以根据源码来推导一下这个过程:

当添加第一个元素时,此时会分配buckets数组和entries数组的空间和初始大小,初始长度为3。对key=1进行哈希求值,假设第一个元素的hashcode=9然后targetBucket=9%buckets.Length(3)的值为0,此时内部结构如图所示:

然后插入第二个元素,对key=2进行哈希求值,假设hashcode=3然后targetBuckets=3%buckets.Length(3)的值为也0,此时就产生了hash冲突,Dictionary<TKey,TValue>解决冲突的方式是链接法,把发生冲突的元素链接在之前元素的后面,通过next属性来指定冲突关系,最后更新哈希表buckets数组。此时内部结构:

 我们可以通过Dictionary<TKey,TValue>查找元素的实现来证明我们上面的分析是正确的。

二.TryGetValue等查找元素方法

从源码中可以看到所有查找元素的方法都调用了FindEntry(key)

private int FindEntry(TKey key) {
	if( key == null) {
		ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
	}

	if (buckets != null) {
		int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
		//buckets[hashCode % buckets.Length]获得元素存储位置,然后遍历entries[i].next
		for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next)
		{
			//直到找到hashcode和key值都匹配的元素
			if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) 
				return i;	
		}
	}
	return -1;
}

 三.Resize()----扩容

有细心的小伙伴可能看过了Add操作以后就想问了,buckets、entries不就是两个数组么,那万一数组放满了怎么办?接下来就是我所要介绍的Resize(扩容)这样一种操作,对我们的buckets、entries进行扩容。
首先能够触发扩容操作的情况有两种,一种是申请的bukets数组容量排满的时候,如下所示:

另一种情况就是当entries数组中多个元素共用一个targetbuket时:

也就是说链表中某个结点后链接的元素太多,那一定会影响我们查找元素的速度,所以Dictionary底层也对这种情况做了处理,这个最大限制是100,大家可以去HashHelpers的源码中看到

Resize源码:

private void Resize(int newSize, bool forceNewHashCodes) {
    Contract.Assert(newSize >= entries.Length);
    // 1. 申请新的Buckets和entries
    int[] newBuckets = new int[newSize];
    for (int i = 0; i < newBuckets.Length; i++) newBuckets[i] = -1;
    Entry[] newEntries = new Entry[newSize];
    // 2. 将entries内元素拷贝到新的entries总
    Array.Copy(entries, 0, newEntries, 0, count);
    // 3. 如果是Hash碰撞扩容,使用新HashCode函数重新计算Hash值
    if(forceNewHashCodes) {
        for (int i = 0; i < count; i++) {
            if(newEntries[i].hashCode != -1) {
                newEntries[i].hashCode = (comparer.GetHashCode(newEntries[i].key) & 0x7FFFFFFF);
            }
        }
    }
    // 4. 确定新的bucket位置
    // 5. 重建Hahs单链表
    for (int i = 0; i < count; i++) {
        if (newEntries[i].hashCode >= 0) {
            int bucket = newEntries[i].hashCode % newSize;
            newEntries[i].next = newBuckets[bucket];
            newBuckets[bucket] = i;
        }
    }
    buckets = newBuckets;
    entries = newEntries;
}

这样就完成了扩容的操作,

这就完成了扩容的操作,但如果是达到Hash碰撞阈值触发的扩容可能扩容后结果会更差。

在JDK中,HashMap如果碰撞的次数太多了,那么会将单链表转换为红黑树提升查找性能。目前.Net Framwork中还没有这样的优化,.Net Core中已经有了类似的优化,以后有时间在分享.Net Core的一些集合实现。

每次扩容操作都需要遍历所有元素,会影响性能。所以创建Dictionary实例时最好设置一个预估的初始大小。

 四、总结

Dictionary<TKey,TValue>之所以能实现快速查找元素,其内部使用哈希表来存储元素对应的位置,我们可以通过哈希值快速地从哈希表中定位元素所在的位置索引,从而快速获取到key对应的Value值。物极必反,Dictionary<TKey,TValue>的缺点也很明显,就是里面的数据是无序排列的,所以按照一定顺序遍历查找数据效率是非常低的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值