一、Hash
将任意大小的数据映射为固定长度数值的算法。
"cat" 通过某个hash算法转化为 12488375
"dog" 通过某个hash算法转化为 98122344
15 通过某个hash算法转化为 15
(1,2) 通过某个hash算法转化为 78213
二、Hash函数(Hash Function)
实现Hash算法的函数就叫Hash函数。
比如一个Hash的算法是将人出生的时、分、秒映射为一个数字,那么实现这个算法的方法叫做Hash函数。
// 伪算法
public int 人出生信息的Hash(出生信息 p)
{
string dataInfo = p.时.ToString()+p.分.ToString()+p.秒.ToString();
return (int)dataInfo;
}
你会发现,Hash的结果可能会有重复,比如上面代码例子中,可能存在多个相同时分秒出生的人。我们称之为hash冲突。
好的哈希函数要求:
快
均匀
可重复,输入不变输出不变,相同 key 必须得到相同 hash
三、Hash冲突不可避免
我可能会想,Hash的key,即Hash函数的入参,是无限多的。
而经过一种Hash方法处理后,返回的数字也可以是无限多无限大的,那么理论上Hash冲突是可以被避免的,可是我为什么说Hash冲突不可避免?
因为:
入参Key的无限多是世界的客观情况,我们改变不了,是我们不能控制的。
但是:
现实中还有个特点,我们的内存是有限的,内存和需求的限制让我们返Hash函数的返回必须是限量的,通常表现为一个定长的字符串,比如16位长、32位长等等。
另外从需求上看,我们需要让无限大无限多的数据DataA压缩为一个短的数据ShortDataA。
无限多不同输入映射到有限多的输出,必然有至少两个输入对应同一个输出。
因此冲突不可避免。
四. 什么是 哈希表(Hash Table)
hash表是一个存储数据的结构。
假如我想存入一个key-value。key是字符串"cat",value是"猫猫"。
哈希表会:
1. 对 key 调用哈希函数,得到一个 hash
// hashKey = 1234;
int hashKey = F(cat);
2.把 hash 对哈希表大小取模(mod),取模就是取余,结果会得到一个余数,余数类似数组的下标,代表位置。
int index = (hashKey % 哈希表的Length)
3.把 key和value 放到这个哈希表的index位置的元素里面,这个元素称之为桶。
bucket[index].Add( new Entry("cat", "猫猫") );
类似于下图看上去哈希表是一个数组,因为存在Hash碰撞,一个桶里面可能会存多个Key-Value,先这样理解:
|---------------------------哈希表----------------------------|
|-------------桶1-------------||-----桶2-----||-----桶2-----||
|{key:cat,value:猫猫} || || ||
|{key:dog,value:狗狗} || || ||
注意,因为cat和dog经过hash方法处理后的结果可能相同,所以经过取模后也会相同,然后落在同一个Bucket桶1里。
五、什么是 哈希桶(Hash Bucket)
哈希桶就是:
哈希表数组中的每个格子,用来存储可能多个 key-value 对。
类似:
|---------------------------哈希表----------------------------|
|-------------桶1-------------||-----桶2-----||-----桶2-----||
|{key:cat,value:猫猫} || || ||
|{key:dog,value:狗狗} || || ||
两个位置落在同一个 bucket = 冲突发生了。
Bucket 本质是:
-
一个链表(最常见)
-
或可扩容的数组
-
或一个 slot
在 Dictionary 中,bucket 里存的是“索引”,不是实际对象。
六、 哈希桶内部如何解决冲突?
hash桶内部是一个单向链表,每插入一个新元素,都会让新节点当头结点,再让让新元素指向老元素。
// bucket[5]d代表下标为5的桶。
bucket[5] → Entry 第三个 → Entry 第二个 → Entry第一个
这时可能有个疑问,链表的时间复杂度不是O(n)吗?为什么向哈希表的插入和查询的时间复杂度是O(1)呢?
链表的时间复杂度确实是O(n),但是当我们的视野提升到哈希表后,哈希表中有个重要概念叫负载因子r,复杂因子的计算是:
r = n / b; n代表数据规模,b代表桶的数量。
哈希表会尽量让每个桶存一个keyvalue,如果当r趋近于x比如0.75,那么就会触发扩容,让桶中的多个链表可能得放在其他桶中。
当大多数桶中链表的元素数量都是1的时候,那么就是O(1)。
七、C# Dictionary 的数据结构
Dictionary 中有两个核心数组:
1.buckets[]
-
大小 = 哈希表大小(bucket 数)
-
每个 bucket 存的是一个整数:entry 的索引
示例:
buckets[0] = -1
buckets[1] = 4
buckets[2] = -1
buckets[3] = 7
2.entries[]
struct Entry
{
int hashCode;
int next; // 链表中下一个 entry 的索引
TKey key;
TValue value;
}
entries 数组里才是真正存 key-value 的地方。
3. 举例
假设哈希表有 8 个 bucket
buckets:
[0]: -1
[1]: 2 → entries[2]
[2]: -1
[3]: 5 → entries[5]
...
entries 数组:
entries:
[2]: {hashCode:1234, next: -1, key:"cat", value:...}
[5]: {hashCode:5678, next: 9, key:"dog", value:...}
[9]: {hashCode:5678, next: -1, key:"god", value:...}
假如我们找bukets下标3的链表,
那么就是通过bucket[3]找到entrys[5],
entrys[5]的next找到entrys[9]。
所有的数据都存在一个数组里。
这就是链地址法,或拉链法。
八、Dictionary 扩容(Rehash)过程
之前说为了保证让插入查询的时间复杂度控制在O(1),引入了复杂因子r,r=数据规模n/桶数量。
当r趋近于0.75或1的时候,触发扩容,目的是尽量让每个桶中链表尽量平均,并且尽量为1.
扩容时,Dictionary 会:
-
计算新的容量:通常是一个更大的数(.NET 早期喜欢用质数,现在多用 2 的幂次或内部算法)。
-
分配新的 buckets 数组 和 entries 数组
-
遍历旧的 entries 数组,对每一个 entry:
newIndex = entry.hashCode % newBuckets.Length;
计算在新的entries数组中的下标,把entriy放到新的位置里。
更新内部引用:以后都用新的 buckets / entries
九、string.GetHashCode() 的算法
遍历字符串每个字符,按某种“乘 + 加”的方式滚动累积。
int hash = 0;
for each char c in string:
hash = hash * A + c; // A 是某个常数,比如 31、397 等
-
不同 .NET 版本 / 平台 / 启动时的随机种子 → 同样的字符串
GetHashCode()结果可能不同。 -
官方明确说:不要把
GetHashCode()的结果存到磁盘/网络,用来长期识别对象。
它只适合在进程内、当前运行时,用于哈希表、Dictionary 等。
十、C# 如何判断两个 key 相等(Equals vs GetHashCode)
哈希表中:GetHashCode() 决定桶,Equals() 决定是不是同一个 key。
当向一个Dictionary插入一个数据的时候,通过GetHashCode%桶长度或的index,确定是哪个桶。
然后在通中,通过key.Equals来和链表中的key做对比,此时相同才说两个Key相等。
在流程中发现,如果K1.Equals(K2)是true,那么K1和K2的hashCode一定相等。
如果K1和K2的HashCode相等,不能保证K1.Equals(K2)是true!
所以当我们自己写的class作为字典的Key的时候,必须同时重写GetHashCode和Equals。
十一、HashTable与Dictionary的区别?
HashTable的key和value的类型是object类型,Dictionary是泛型,存object类型可能存在装箱拆箱,HashTable性能弱。
HashTable和Dictionary在代码中都有Bucket数组,Hashtable中的实体是Slot结构体,Slot中的keyvalue成员变量是object类型。Dictionary中的实体是entry结构体,Entry的keyvalue类型是泛型T。
struct Slot {
public int hashCode; // 哈希
public object key; // key(object,不是泛型)
public object value; // value
public int next; // 单向链表下一项
}
struct Entry{
public int hashCode; // 哈希
public T key; // key(泛型)
public T value; // value
public int next; // 单向链表下一项
}
HashTable是弱类型,Dictionary是强类型:
Hashtable ht = new Hashtable();
ht["age"] = 15;
ht["name"] = "cat";
ht[100] = "weird"; // 允许这种奇怪的 key
Dictionary<string, int> dict; // key 必须是 string,value 必须是 int
1064

被折叠的 条评论
为什么被折叠?



