【数据结构and算法】杂记
数据结构
浅析Dictionary
Dictionary(字典)是C#的一个集合类,它可以通过Key/Value(键值对)的形式存放数据,是C#另一个集合类hashtable(哈希表)的泛型实现。它相对于其他数据结构如Array、List最大的优势是:查找元素的时间复杂度接近O(1)。
1 理论
1.1 hash函数
hash函数帮助实现了时间复杂度为O(1)的高效查找。它将任意大小的输入(Key)转换为固定大小的输出(通常是整数),用于确定键值对在哈希表中的存储位置。
1.2 hash碰撞
hash函数的其中一个特点是:不同数据经过hash计算后,其结果可能相同。这种现象称为哈希碰撞/哈希冲突。
1.3 解决hash碰撞
常见的解决冲突的算法有:
- 开放定址法
- 线性探测法:每次冲突后检查下一个连续的槽位,直到找到一个空槽。
- 平方探测法:探测序列的间隔不是固定的,而是依次增大,通常按照平方增长。
- 双重哈希:使用第二个哈希函数计算步长,确保探测序列的分布更均匀。
- 拉链法(Dictionary采用的方法)
- 在每个哈希槽位中,存储一个链表,将所有映射到同一槽位的元素存储在这个链表中。
- 缺点:如果有很多冲突,链表可能变长,查找、插入、删除的平均时间复杂度会增加到O(n)(在最坏情况下)。
- 优化:使用平衡二叉树(如红黑树)或其他数据结构替代链表,可以在冲突严重时优化性能。
- 再哈希法
- 发生冲突就再次使用另一个哈希函数计算地址直到找到地址为止。
- 扩展哈希表
- 当冲突频繁或者哈希表的负载因子过高时,增加哈希表的大小,然后重新哈希所有的元素到新的表中。
- …
2 Dictionary的底层实现
2.1 部分参数
2.1.1 Entry结构体
1 | private struct Entry { |
2.1.2 其他关键变量
1 | private int[] buckets; // Hash桶 |
2.2 初始化
1 | // HashTable中预存的int类型的所有质数 |
2.3 Add
假设当前的Dictionary是这样的:
2.3.1 正常添加
1 | dict.Add("a","b"); |
接着会进行以下操作(这里的步骤并不完善,这样写是方便理解):
1 | //1.根据Key的值,计算对应的hashcode,这里我们假设结果为6 |
完成以上操作后,Dictionary更新为:
2.3.2 哈希碰撞的解决
我们继续执行Add操作。
1 | dict.Add("c","d"); |
这里我们假设GetHashCode("c") = 6
,执行步骤1、2后桶的索引为2,此时buckets[2]!=-1,代表桶中已有数据,哈希碰撞发生。
此时Dictionary会在步骤3时执行这样的操作:
1 | //... |
2.4 Remove
1 | public bool Remove(TKey key) { |
2.5 Resize(扩容)
Dictionary扩容时,会申请新的容量的buckets、entries数组,然后将现有的元素拷贝到新的entries里。
需要注意的是,此时buckets和entries之间的映射丢失,需要重新建立:遍历entries,根据其中的hashcode,重新建立映射,next也需要重新修改。
2.6 version的作用
帮助避免在迭代过程中发生集合修改:
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 manqi!
评论