今年跳槽到另一家公司,总结下这次经历过的面试点,适用于要求1~2年工作经验的岗位。
字典篇里的理论知识部分主要摘抄自网上文章浅析C# Dictionary实现原理,感谢原作者的无私分享。之后结合C# 源码总结了自己对关键结构实现的理解,C#源代码传送门 https://referencesource.microsoft.com/#mscorlib
字典的实现
关键原理
(一)哈希算法
哈希算法是一种数字摘要算法,它能将不定长度的二进制数据集给映射到一个较短的二进制长度数据集;使用了哈希算法的函数称为哈希函数。
常用的哈希算法:
- 直接寻址法:取keyword或keyword的某个线性函数值为散列地址。即H(key)=key或H(key) = a•key + b,当中a和b为常数(这样的散列函数叫做自身函数)。
- 数字分析法:分析一组数据,比方一组员工的出生年月日,这时我们发现出生年月日的前几位数字大体同样,这种话,出现冲突的几率就会非常大,可是我们发现年月日的后几位表示月份和详细日期的数字区别非常大,假设用后面的数字来构成散列地址,则冲突的几率会明显减少。因此数字分析法就是找出数字的规律,尽可能利用这些数据来构造冲突几率较低的散列地址。
- 平方取中法:取keyword平方后的中间几位作为散列地址。
- 折叠法:将keyword切割成位数同样的几部分,最后一部分位数能够不同,然后取这几部分的叠加和(去除进位)作为散列地址。
- 随机数法:选择一随机函数,取keyword的随机值作为散列地址,通经常使用于keyword长度不同的场合。
- 除留余数法:取keyword被某个不大于散列表表长m的数p除后所得的余数为散列地址。即 H(key) = key MOD p, p<=m。不仅能够对keyword直接取模,也可在折叠、平方取中等运算之后取模。对p的选择非常重要,一般取素数或m,若p选的不好,容易产生碰撞。
C#源码中字符串类型使用的哈希算法是直接寻址法+折叠法:设置hashcode1和hashcode2一个相同的值,每两个字符使用直接寻址更新hashcode1和hashcode2的值,最后hashcode1和hashcode2叠加。
(二)哈希冲突
不同的数据通过哈希算法映射出来的值可能会一样,称为哈希冲突。
常用的解决办法:
- 拉链法:使用单链表的结构连接所有的冲突数据,映射后的值是链表的头结点,遍历链表比对数据的哈希值与keyword值获得数据。
- 再哈希法:将keyword使用其它的哈希函数再次哈希,直到找到不冲突的位置为止。
以上两种方法C#源码中都有使用
(三)哈希桶
如果直接使用哈希值映射,会使保存哈希值的数组过长而且没有充分利用空间,因此会将哈希值以分段的形式映射,分段形式的数组称为哈希桶。光看原理可能不太好理解,结合之后的代码看会容易理解很多。
方法:保存一个数组作为哈希桶,数组长度为大于或等于字典容量的质数;当查找数据时,对哈希值取余,除数为哈希桶的长度,余数为数据对应的数据数组下标。
优点:哈希值一般是2^32位,避免直接使用哈希值映射数据导致映射数组长度过长。
缺点:增加哈希冲突。
注意:桶的个数使用质数,可以最大程度减少冲突概率,使哈希后的数据分布的更加均匀。如果使用合数,可能会造成很多数据分布会集中在某些点上,从而影响哈希表效率。
上图解释了字典的关键原理,buckets代表了哈希桶,entries下使用了拉链法解决哈希冲突
关键代码实现
(一)存放数据的结构
hashcode是32位无符号数,所以赋值时第32位要保证是0。
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
}
(二)关键变量
public class Dicitonary<TKey, TValue> {
private Entry[] entries; //Array of data
private int[] buckets; //Array of data index by cast hash value
private int count; //Tail index of data array
private int freeList; //Index of data which is free
private int freeCount; //Count of fress data
private int version; //version of code
private IEqualityComparer<TKey> comparer; //comparer of key
}
(三)关键函数
1. 初始化
使用大于或等于字典容量的质数设置buckets和entries的长度。
private void Initialize(int capacity)
{
int size = HashHelpers.GetPrime(capacity);
buckets = new int[size];
for (int i <