C#容器类的原理解析(二)

上个博文记录了hashtable的执行原理,这个博文谈谈dictionary的执行过程。

简单来说,dictionary使用两个数组来通过hash存储键值对并且避免冲突的过程。分别是Entries数组和Bucket数组,其中entries数组中保存哈希code和键、值,以及一个重要的int型的next的值,就是用这个next值来避免冲突。bucket数组中的索引用来保存对应的hashcode在哪个entries数组中去找键值对。

下面看看entries数组和bucket数组的结构:

简单描述一下dictionary增加元素的过程:

按顺序执行以下代码,那么每一步,源码底层做了哪些事情?

Dictionary dic = new Dictionary();

dic.Add(1,"a");

dic.Add(2,"b");

dic.Add(2,"c");

将上一步改为dic.Add(4,"c");

var value = dic[1];

1.由于没有给定长度,初始化时,dic的entries数组长度为0,bucket数组长度为0,假如给定为10,则二者长度都为11(为大于10的最大素数)

2.加入新键值对,两个数组都扩容为3(0~2)(其实是先算1的哈希值,并且判断没有该哈希值或者有该哈希值但是是不同的键才会认定需要增加该键值对,才会扩容),1的哈希值为1,对3取余为1,且bucket[1]为空(0),所以将该键值对和哈希值放在entries[0]中,并将bucket[1]的值设为1,表示第一个entries中存储了哈希值为1的键值对(也就是说bucket数组中的值减一才是对应的entries数组的索引,为什么这么做,因为在没有值的情况下是0,所有bucket数组中的0值毫无意义,所以要从1开始)。

3.计算2的哈希值为2,并且bucket[2]为空,所以将该键值对放入entries[1]中(entries就是按顺序放入的),并且bucket[2]=2,表示键哈希值为2的键值对存储在第二个entries数组的结构中。

4.计算2的哈希值为2,并且bucket[2]=2,于是得到key = entries[1].key,得到key = 2,于是抛出异常,键重复。

5.计算4的哈希值为4,并且对bucket数组长度取余(3)得到1,然而key = bucket[1].key,得到key =  1,得到该键为1不等于4,说明出现了哈希冲突。这里是怎么解决的呢?

首先,该键值对是合法的(没有重复键)那么假如entries数组,也就是entries[2]中放入键值对为2,“c”的结构,同时,注意entry结构中的next值默认是-1,在这里,将entries[2].next = bucket[1],也就是说,由于我覆盖了bucket[1]的原本的索引也就是entries[0],因而微软将这里的entries[2]的next值赋值为该索引也就是0;然后bucket[1]的值就改为3,表示哈希值为2的键值对存储在entries数组中的第三个结构中也就是entries[2]

该解决哈希冲突的方法叫做“拉链法”。

6.计算1的哈希值为1,对bucket数组取余为1,得到bucket[1] = 3,进而去找entries[2],得到键值为4,不等于1,于是找next的值,该值为0,于是在entries[0]中找到键值为1的键值对,其值为“a”,于是返回a

当继续增加元素时,bucket和entries都要扩容,翻倍成为大于自身两倍的最小素数,并且将所有元素全部重新哈希,按照上面的计算方式重新分配bucket数组中的值,entries数组拷贝到一个更大的数组空间中去。

所以由此分析,仅仅从resize扩容的角度来看,两者的效率是差不多的,都需要扩容并且rehash,dictionary还需要重建链,但是由于hashtable存在类型转换,所有元素都是object类型,因而一般来说dictionary比hashtable在添加元素上效率差不多,但是对应于查找元素,hashtable只需要一次(或者多次)hash就可以找到,然而dictionary可能还需要遍历链,所以hashtable的查找效率更高。

发布了58 篇原创文章 · 获赞 7 · 访问量 3724

猜你喜欢

转载自blog.csdn.net/xy_learning/article/details/105211901