为什么String.hashcode()有这么多冲突?
我正在读jdk1.6中的String.hashCode(),下面是代码
public int hashCode() {
int h = hash;
if (h == 0) {
int off = offset;
char val[] = value;
int len = count;
for (int i = 0; i < len; i++) {
h = 31*h + val[off++];
}
hash = h;
}
return h;
}
这对我来说很混乱,因为它有很多冲突;虽然它不需要是唯一的(我们仍然可以依赖于equals()),但是更少的冲突意味着更好的性能,而无需访问链表中的条目。
假设我们有两个字符,那么只要我们找到两个匹配下面的方程的字符串,那么我们就会有相同的hashcode()
a * 31 +b = c * 31 +d
很容易得出结论(a-c) * 31 = d-b
举一个简单的例子是a-c = 1和d-b = 31;
所以我在下面写了简单测试代码
public void testHash() {
System.out.println("A:" + (int)'A');
System.out.println("B:" + (int)'B');
System.out.println("a:" + (int)'a');
System.out.println("Aa".hashCode() + "," + "BB".hashCode());
System.out.println("Ba".hashCode() + "," + "CB".hashCode());
System.out.println("Ca".hashCode() + "," + "DB".hashCode());
System.out.println("Da".hashCode() + "," + "EB".hashCode());
}
它将打印在结果下面,这意味着所有字符串都具有相同的hashcode(),并且很容易在循环中完成。
A:65
B:66
a:97
2112,2112
2143,2143
2174,2174
2205,2205
更糟糕的是,假设我们在字符串中有4个字符,根据算法,假设前2个字符产生a2,第2个字符产生b2;
哈希码仍然是a2 * 31^2 + b2
因此,当a2和b2在2个字符串之间相等时,我们将获得更多具有hashcode()冲突的字符串。
这样的例子是“AaAa”,“BBBB”等;
那么我们将有6个字符,8个字符......
假设大部分时间我们在字符串中使用ascii表中的字符将在hashmap或hashtable中使用,那么这里选择的素数31肯定太小;
一个简单的解决方法是使用更大的素数(幸运的是,257是素数)可以避免这种冲突。当然,选择一个太大的数字会导致返回的int值溢出,如果字符串很长,但我假设大多数时候用作键的字符串不是那么大? 当然,它仍然可以返回一个很长的值来避免这种情况。
下面是我更好的betterhash()版本,它可以轻松解决此类冲突 通过运行代码,它将打印在值以下,这有效地解决了这个问题。
16802,17028
17059,17285
17316,17542
17573,17799
但是为什么jdk没有解决它?谢谢。
@Test
public void testBetterhash() {
System.out.println(betterHash("Aa") + "," + betterHash("BB"));
System.out.println(betterHash("Ba") + "," + betterHash("CB"));
System.out.println(betterHash("Ca") + "," + betterHash("DB"));
System.out.println(betterHash("Da") + "," + betterHash("EB"));
}
public static int betterHash(String s) {
int h = 0;
int len = s.length();
for (int i = 0; i < len; i++) {
h = 257*h + s.charAt(i);
}
return h;
}
答案 0 :(得分:40)
我刚刚练习了58,000个英语单词(找到here),全都是小写的,第一个字母大写。知道有多少相撞?二:“兄弟姐妹”和“德黑兰”(“德黑兰”的替代拼写)。
就像你一样,我拿了一个子域(在我的情况下可能是一个)可能的字符串并分析了它的hashCode冲突率,并发现它是典型的。谁能说你的可选字符串的任意子域是比我的优化更好的选择?
编写此类的人必须这样做,因为他们无法预测(也不会因此优化)其用户将字符串用作键的子域。所以他们选择了一个散列函数,它在整个字符串域上均匀分布。
如果您有兴趣,这是我的代码(它使用Guava):
List<String> words = CharStreams.readLines(new InputStreamReader(StringHashTester.class.getResourceAsStream("corncob_lowercase.txt")));
Multimap<Integer, String> wordMap = ArrayListMultimap.create();
for (String word : words) {
wordMap.put(word.hashCode(), word);
String capitalizedWord = word.substring(0, 1).toUpperCase() + word.substring(1);
wordMap.put(capitalizedWord.hashCode(), capitalizedWord);
}
Map<Integer, Collection<String>> collisions = Maps.filterValues(wordMap.asMap(), new Predicate<Collection<String>>() {
public boolean apply(Collection<String> strings) {
return strings.size() > 1;
}
});
System.out.println("Number of collisions: " + collisions.size());
for (Collection<String> collision : collisions.values()) {
System.out.println(collision);
}
顺便说一句,如果你很好奇,与你的哈希函数相同的测试与String.hashCode
的1相比有13次碰撞。
答案 1 :(得分:12)
对不起,我们需要对这个想法嗤之以鼻。
您的分析太简单了。你似乎已经挑选了一系列字符串,旨在证明你的观点。这并不是证据表明在所有字符串的范围内,冲突的数量(统计上)高于预期。
没有人会在期望 String.hashCode高度无冲突。它的设计并没有考虑到这一点。 (如果你想要高度无冲突的散列,那么使用加密散列算法......并支付费用。)String.hashCode()被设计为在所有字符串的域中相当不错......并且快速< /强>
假设你可以陈述一个更强的案例,这不是陈述它的地方。您需要向重要人员提出此问题 - Oracle的Java工程团队。
Java工程团队将权衡这种变化的优势与实现它的成本,以及其他所有Java用户。最后一点可能就足以杀死这个石头了。
(“高度无冲突散列”,是我为了这个答案而撤出的一个想法/术语。抱歉。但是,要点是2个字符串的哈希码冲突概率因此,例如,“AA”和“bz”由于具有相同的长度而相关。显然,这个想法需要更多的思考。并且在这个意义上也很明显“相关性”我说的是不可测量的...有点像Kolmogorov Complexity。)
答案 2 :(得分:8)
散列时碰撞是不可避免的。 hashCode()
方法返回一个整数,该整数用作数组的索引,该数组是具有相同哈希码的所有对象的存储桶。 equals(Object)
方法用于将目标对象与存储桶中的每个对象进行比较,以识别完全匹配的对象(如果存在)。
最终,hashCode()
方法只需快而不是太弱(即造成太多碰撞),其中太弱非常模糊度量。
答案 3 :(得分:1)
效率很高但也很简单。所有可能的小写(ASCII)字最多六个字母或所有数字最多六个数字长都有一个唯一的hashCode()。即,hashCode类似于基数31。使用更大的数字有其自身的问题。 257因子会使每第8位不是特别随机,因为所有ASCII字符都有0个最高位。较大的因素会导致五个和六个数字/字母单词的重复哈希码。
如果无法更改散列算法,可能是最大的问题。无论你采取什么方法,都可能出现这样一个非常糟糕的选择,并且它可能对你的用例来说是次优的。
也许最大的问题是拒绝服务攻击造成病态病例,通常非常罕见很常见。例如,攻击Web服务器的方法是使用具有相同hashCode的密钥填充缓存,例如, 0每次计算。这会导致HashMap退化为链表。
解决这个问题的一个简单方法是使哈希算法未知,可能会改变。作为它的立场,最好的可能是使用TreeMap(它支持自定义比较,但在这种情况下默认会没问题)