哈希概念
哈希(Hash),又称散列,本质上是一种组织数据的方式。它的核心思想是通过哈希函数建立关键字 Key 与存储位置之间的映射关系。查找时,只需通过同样的哈希函数计算出 Key 对应的存储位置,即可快速定位数据。
基于这种映射思想,数据结构中实现了哈希表(散列表)。需要注意的是,哈希是一种思想,而哈希表是实现该思想的特定数据结构,二者不能简单等同。
哈希冲突与哈希函数
假设我们要将 N 个值映射到大小为 M 的数组中(通常 M >= N),就需要借助好的映射方法,使关键字 key 被放到数组的 h(key) 位置。这里必须保证 h(key) 的计算结果在 [0, M) 之间。
一个不可避免的问题是,两个不同的 key 可能会映射到同一个位置,这就是哈希冲突(或哈希碰撞)。我们将数据与存储位置之间的映射表达式称为哈希函数(hash function)。一个好的哈希函数应让 N 个关键字等概率、均匀地散列分布到哈希表的 M 个空间中。虽然实际中很难做到完美的等概率,但设计时应尽量往这个方向靠拢。
负载因子
假设哈希表中已存储了 N 个值,哈希表大小为 M,那么 N/M 就是负载因子(Load Factor)。
负载因子越大,哈希冲突的概率越高,但空间利用率也越高;反之,负载因子越小,冲突概率越低,但空间浪费越多。在实际工程中,我们需要在冲突率和空间占用之间寻找平衡点。
关键字转为整数
哈希函数的计算通常基于整型。如果关键字不是整型(例如字符串),我们需要想办法将其转换为整型。这一细节将在后续代码实现中展示。讨论哈希函数时,若关键字非整数,文中的 Key 均指转换后的整数值。
哈希函数设计
在实际应用中,我们根据具体场景确定如何映射。为了后续能准确找到数据,插入、查找等一系列过程必须使用同一个哈希函数,中途不能更改,否则会导致前后不一致。
直接定址法
当关键字范围比较集中时,直接定址法非常高效。例如关键字都在 [0, 99] 之间,可以直接开一个 100 个元素的数组,每个关键字的值直接作为下标。
对于字符类型,比如小写字母 [a, z],可以开一个 26 个元素的数组,利用 ascii 码 - 'a' 的差值作为相对位置。这本质是用关键字计算出绝对或相对位置。这种方法在计数排序和某些字符串处理场景中很常见。
缺点也很明显:如果关键字范围分散(例如最大值与最小值差值达 10 亿,但只有 20 个数据),直接开辟大数组会极度浪费内存甚至导致内存不足。对于分散数据,需采用其他映射方式。
除法散列法(除留余数法)
这是最常用的方法之一。假设哈希表大小为 M,哈希函数为:h(key) = key % M。
注意事项:
- 尽量避免 M 取 2 的幂或 10 的幂。如果是 2 的幂,
key % M相当于保留二进制后 X 位,若低位相同则冲突严重。 - 建议 M 取不太接近 2 的整数次幂的质数。
- 实践中也有例外,如 Java HashMap 采用 2 的幂做大小,配合位运算优化效率,并通过异或操作让高位参与计算来减少冲突。
总的来说,映射出的值仍在 [0, M) 范围内,关键是尽量让 key 的所有位都参与计算,使分布更均匀。
乘法散列法
对哈希表大小 M 没有严格要求。思路是:
- 用关键字 K 乘以常数 A (0 < A < 1),抽取小数部分。
- 再用 M 乘以该小数部分并向下取整。
公式:h(key) = floor(M × ((A × key)%1.0))。 Knuth 建议 A 取黄金分割点相关值(约 0.618),实验表明这能使数据分布较均匀。
全域散列法
为了防止恶意攻击者针对确定的散列函数构造冲突数据集,可以给散列函数增加随机性。即从一组函数中随机选取一个使用,后续增删查改固定使用该函数。
公式示例:h(key) = ((a × key + b)%P )%M。其中 P 为大质数,a、b 随机选取。这样攻击者无法预先构造最坏情况的数据集。
处理哈希冲突
无论选择什么哈希函数,只要 M 小于数据量,冲突就不可避免。解决冲突主要有两种策略:开放定址法和链地址法。


