过去几个小时我一直在阅读有关哈希码函数的内容,并且在自定义哈希码实现中使用素数作为乘数已经积累了一些问题。如果我能对以下问题有所了解,我将不胜感激:
在这里对@mattb's answer的评论中,@ hstoerr主张使用更大的素数(例如524287)而不是公共素数31.我的问题是,给定以下哈希码函数的实现一对或多个元素:
@Override
public int hashCode() {
final int prime = 31;
int hash1 = (pg1 == null) ? 0 : pg1.hashCode();
int hash2 = (pg2 == null) ? 0 : pg2.hashCode();
return prime * (hash1 ^ hash2);
}
int
是一个大数字,这不会导致返回prime
上的溢出?
假设溢出不是问题(JVM进行自动转换)是不是更好的做位移而不是转换?
我认为哈希码函数的性能会因哈希码的复杂性而有很大差异。主乘数的大小是否会影响性能?
在自定义哈希码函数中使用多个素数而不是单个乘数更好/更聪明/更快?如果没有,还有其他一些优势吗?请参阅以下示例@ jinguy对a relevant question的回答:
public int hashCode() {
return a * 13 + b.hashCode() * 23 + (c? 31: 7);
}
其中a
为int
,b
为String
,c
为boolean
。
long lhash = prime * (hash1 ^ hash2);
之类的内容如何使用(int)((lhash >> 32) ^ lhash)
?这是我在另一个问题上看到的东西,但是并没有真正解释为什么这样做是个好主意。答案 0 :(得分:7)
为小说提前道歉。随意提出建议或直接编辑。 --Chet
有溢出,但也不例外。
危险并非来自失去准确性,而是失去范围。让我们使用一个荒谬的例子,其中" prime"为简洁起见,它是2的大功率和8位无符号数。并假设(hash1 ^ hash2)
为255:
"prime": 1000 0000
(hash1 ^ hash2): 1111 1111
在括号中显示截断的数字,结果是:
product: [0111 1111] 1000 0000
但乘以128与左转7位相同。所以我们知道无论(hash1 ^ hash2)
的价值如何,产品中最不重要的位置都会有七个零。因此,如果(hash1 ^ hash2)
为奇数(最低有效位= 1),则乘以128的结果将始终为128(在截断较高位数之后)。如果(hash1 ^ hash2)
是偶数(LSB为0,则产品将始终为零。
这扩展到更大的位大小。一般的观点是,如果" prime
"的低位是0,你正在做一个移位(或多次移位+求和)操作,它会在低位给你零。并且乘法乘积的范围将受到影响。
但是,让我们尝试制作" prime
"奇数,因此最低有效位将始终为1.考虑将其分解为shift / add操作。 (hash1 ^ hash2)
的未移位值始终是其中一个加数。被偶数" prime
"转移到保证无用的最低有效位现在,乘数将至少根据原始(hash1 ^ hash2)
值中的位进行设置。
现在,让我们考虑prime
的值,它实际上是素数。如果它超过2,那么我们就知道它很奇怪。所以低位还没有变成无用的东西。通过选择足够大的素数,您可以在输出值范围内获得比在较小素数下获得更好的分布。
尝试使用8443(0010 0000 1111 1011
)和59(0000 0000 0011 1011
)进行16位乘法练习。它们都是素数,59的低位与65531的低位匹配。例如,如果hash1和hash2都是ASCII字符值(0 ... 255),则所有结果(hash1 ^ hash2) )* 59将是< = 15045.这意味着16位数字的大约1/4的散列值范围(0..65535)未被使用。
但(hash1 ^ hash2) * 8443
遍布地图。如果(hash1 ^ hash2)
低至8,它会溢出。即使对于非常小的输入数字,它也会使用所有16位。即使输入数字的范围相对较小,整个范围内的哈希值聚类也要少得多。
假设溢出不是问题(JVM进行自动转换)是否更好的做位移而不是转换?
很可能不是。无论如何,JVM应该转化为主处理器上的有效实现。整数乘法应该在硬件中实现。如果没有,JVM负责将操作转换为适合CPU的操作。整数乘法的情况很可能已经高度优化。如果在给定的CPU上作为shift-and-add更快地完成整数乘法,那么JVM应该以这种方式实现它。但是,编写JVM的人不太可能关注多个移位和添加操作可以组合成单个整数的情况。
我认为哈希码函数的性能会因哈希码的复杂性而有很大差异。尺寸 主乘数不影响绩效?
没有。无论大小,设置的位数等等,在硬件中完成的操作都是相同的。它可能是几个时钟周期。它会根据特定的CPU而有所不同,但无论输入值如何,都应该是一个恒定时间操作。
在自定义哈希码函数中使用多个素数而不是单个乘法器是否更好/更智能/更快?如果没有,是吗? 其他一些优势?
只有降低了碰撞的可能性,这取决于您使用的数字。如果您的哈希码取决于A
和B
并且它们处于相同的范围内,您可以考虑使用不同的素数或移位其中一个输入值以减少这些位之间的重叠。由于您依赖于各自的哈希码而不是它们的值,因此可以合理地假设它们的哈希码提供了良好的分布等。
考虑到您希望(x, y)
的哈希码与(y, x)
不同的一个因素。如果您的哈希函数以相同的方式处理A
和B
,那么hash(x, y) = hash(y, x)
。如果那是你想要的,那么一定要使用相同的乘数。不是,使用不同的乘数是有意义的。
long lhash = prime * (hash1 ^ hash2);
之类的内容如何使用(int)((lhash >> 32) ^ lhash)
?这是我在另一个问题上看到的东西,但是它并没有真正解释为什么这样做是个好主意。
有趣的问题。在Java中,long是64位,而int是32位。因此,这会根据需要使用两倍的位生成一个哈希值,然后从高位和低位组合得到结果。
如果将数字n
乘以素数p
,并且k
的最低n
位全部为零,则最低k
位为产品n * p
也将全部为零。这很容易看出 - 如果您将n = 0011 0000
和p = 0011 1011
相乘,那么产品可以表示为两个班次操作的总和。或者,
00110000 * p = 00100000 * p + 00010000 * p
= p << 5 + p << 4
采用p = 59
并使用无符号8位整数和16位长整数,这里有一些例子。
64: 0011 1011 * 0100 0000 = [ 0000 1110 ] 1100 0000 (192)
128: 0011 1011 * 1000 0000 = [ 0001 1101 ] 1000 0000 (128)
192: 0011 1011 * 1100 0000 = [ 0010 1100 ] 0100 0000 (64)
通过仅丢弃结果的高位,当非素数被乘数的低位全为零时,得到的散列值的范围受到限制。这是否是特定上下文中的问题,特定于上下文。但是对于一般的散列函数,即使输入数字中存在模式,也避免限制输出值的范围是个好主意。在安全应用程序中,避免任何可能让某人根据输出中的模式推断原始值的做法更为重要。只取低位就会显示一些原始位的确切值。如果我们假设操作涉及将输入数与大素数相乘,那么我们就知道原始数字在右边有与哈希输出一样多的零(因为素数最右边的位是1)。
通过使用低位对高位进行异或,输出的一致性较差。更重要的是,根据这些信息对输入值进行猜测要困难得多。根据XOR的工作原理,可能意味着原始低位为0,高位为1,或原始低位为1,高位为0.
64: 0011 1011 * 0100 0000 = 0000 1110 1100 0000 => 1100 1110 (206)
128: 0011 1011 * 1000 0000 = 0001 1101 1000 0000 => 1001 1101 (157)
192: 0011 1011 * 1100 0000 = 0010 1100 0100 0000 => 0110 1100 (204)
答案 1 :(得分:3)
溢出不是问题。无论如何,哈希都被限制在一个较窄的值集中。
您发布的第一个哈希函数不是很好。做return (prime * hash1) ^ hash2;
`相反,在大多数情况下会减少碰撞次数。
乘以单个字int通常非常快,乘以不同数字之间的差异可以忽略不计。此外,执行时间与函数中的其他所有内容相比相形见绌
为每个部分使用不同的素数乘数可以降低碰撞的风险。