前言
哈希表是⼀种查找性能⾮常优异的数据结构,它在计算机系统中存在着⼴泛的应⽤。
尽管哈希表理论上的查找时间复杂度是 O(1),但不同的哈希表在实现上仍然存在巨⼤的性能差异。
下面是jdk里的一些hash表的测试情况
| Key数量 | 碰撞率 | JDK HashMap内存占用 | FastUtil内存占用 |
|---|---|---|---|
| 100万 | 20% | 1.2GB | 860MB |
| 100万 | 50% | 2.8GB | 1.4GB |
我们发现,hash频繁碰撞也有可能将查询时间复杂度从O(1)退化为O(n),空间复杂度也会伴随上升。
这是因为,一般哈希表对哈希冲突的处理会增加额外的分⽀跳转和内存访问,这会让流⽔线式的CPU指令处理效率变差。当遇到哈希冲突时,我们常见到的解决⽅案有:开放寻址法、拉链法、⼆次哈希法。
场景
本文所有代码均用kotlin展示,不习惯者可以用AI转一下java
现在我们有个如下的函数
class EntityWrapperKey<HASH>(value: EntityWrapper<*, HASH>) {
val pk: Serializable = value.getPrimaryKey()
val clazz: Class<*> = value.javaClass
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other?.javaClass != javaClass) return false
other as EntityWrapperKey<HASH>
if (pk != other.pk) return false
if (clazz != other.clazz) return false
return true
}
}
我们先实现一下标准哈希计算模式,满足基本需求
// 使用31作为乘法因子,结合pk和clazz的哈希码
override fun hashCode(): Int {
var result = pk.hashCode()
result = 31 * result + clazz.hashCode()
return result
}
缺点:
复合主键深度问题:若 pk 是嵌套对象,其 hashCode() 可能未覆盖所有字段(如使用默认对象哈希)- 乘数选择局限:31 作为经典乘数在小数据量表现良好,但在
百万级数据下碰撞率可能超过 15% - 计算效率:两次乘法操作在频繁调用时可能成为性能瓶颈
优化方案与代码实现
怎么能完全规避哈希冲突?那么有没有完美哈希函数(perfect hash function)呢?
1. 预计算哈希值(内存换性能)
0x9E3779B9 是一个非常著名的常数,在哈希函数和伪随机数生成器中广泛使用。这个常数源自于黄金分割比例。在计算机科学中,这个常数通常用于确保哈希函数的输出分布更加均匀,减少冲突的概率。
class EntityWrapperKey<HASH>(value: EntityWrapper<*, HASH>) {
private val cachedHash by lazy {
computeOptimizedHash() }
private fun computeOptimizedHash(): Int {
var hash = 0x9E3779B9 // 黄金分割常数
hash = hash * 31 + clazz.hashCode()
hash = hash xor (pk.hashCode().rotateLeft(5)) // 位操作混合
return hash
}
override fun hashCode()

最低0.47元/天 解锁文章
173万+

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



