面经-Hash、Hash表、Hash桶到Dictionary

一、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 会:

  1. 计算新的容量:通常是一个更大的数(.NET 早期喜欢用质数,现在多用 2 的幂次或内部算法)。

  2. 分配新的 buckets 数组 和 entries 数组

  3. 遍历旧的 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值