哈希冲突让面试者崩溃:出门右转,找下家吧!

270 阅读5分钟

一、哈希冲突的定义

哈希冲突,顾名思义,是指在哈希表中,两个或多个不同的输入键经过哈希函数计算后得到了相同的哈希值,导致它们被映射到了同一个存储位置上。这种情况的发生是不可避免的,因为哈希函数的输出范围(即哈希表的大小)通常是有限的,而输入空间(即可能的键的数量)往往是无限的。 由于哈希在开发中经常用到,包括数据结构中的Map,Redis的hash类型,因此在面试中也经常被提到为什么会哈希冲突,本文将详细讲解哈希冲突的原因以及实际工作如何避免。

二、Java中的hashCode方法

在面试中,面试官首先会问hashCode是哪个类的方法?

如果没有看有源码的同学,可能会直接说String类,那么面试官可能就不想继续面试,只能出门右转了。

其实在Java中,hashCode方法是java.lang.Object类的一个成员方法,并且是使用native修饰,所有的Java对象都继承自这个类。默认情况下,hashCode方法返回对象的内存地址的一个整数表示,具体值也是跟对象头信息有关。

image.png

但是,Java也是允许重写hashCode方法,主要是为了避免冲突。例如,String类就重写了hashCode方法,它实现方式是将字符串中的每个字符乘以一个质数(通常是31),然后将所有结果相加得到最终的哈希码。这种方法可以在一定程度上减少哈希冲突的发生。

image.png

其实计算规则如下:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

三、哈希冲突案例分析

如果上面问题能够正确回答,那么面试官可能让你写一个哈希冲突的例子,以考验是否真正了解哈希冲突。

案例一:不同字符串的哈希码相同

如果有准备而来,或者真的去了解过hashcode,在jdk内部静态变量中可能某些字符串的hashcode是一直,又或者自己尝试算过某两个字符串计算的hashcode值是一样,那么就可以投机取巧的回答直接输出这两个字符串,比如下面案例。


System.out.println("Aa".hashCode());
System.out.println("BB".hashCode());

在这个例子中,可以看到两个不同的字符串"Aa"和"BB"具有相同的哈希码。这其实就是一种典型的哈希冲突情况。这种情况的发生可能与String类的hashCode方法的实现方式有关,可以手动计算一下。

对于字符串Aa和BB,Java的String类重写了hashCode()方法,使用以下算法计算哈希码:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

其中s[i]是字符串中第i个字符的Unicode值,n是字符串的长度。

现在,看看为什么"Aa"和"BB"的哈希码都是2112:

对于字符串"Aa":

  • 第一个字符'A'的Unicode值是65。
  • 第二个字符'a'的Unicode值是97。

应用上述公式:

65 * 31^1 + 97 * 31^0 = 65 * 31 + 97 = 2015 + 97 = 2112

对于字符串"BB":

  • 第一个字符'B'的Unicode值是66。
  • 第二个字符'B'的Unicode值也是66。

应用上述公式:

66 * 31^1 + 66 * 31^0 = 66 * 31 + 66 = 2046 + 66 = 2112

最终运行结果如下,刚好跟计算的结果一样,当然也可以记住这个两个字符串Aa和BB会产生哈希冲突。 image.png

案例二:循环创建对象并检测哈希冲突

如果资历较浅的面试官,上面回答之后可能就不继续深究了,但是有些较真(exin)的面试官就会觉得上面回答有点背题而来,没有凸显自己能力,就会继续发问。

那么就只能放大招了,只能编写真正产生哈希冲突的实例代码。 可以通过循环创建了大量的Person对象,并将每个对象的哈希码添加到一个HashSet集合中。如果在添加过程中发现某个哈希码已经存在于集合中,那么就说明发生了哈希冲突。具体代码如下:

public static void main(String[] args) {
    HashSet<Integer> hashSet = new HashSet<>();
    for (int i = 1; i <= 11 * 10000; i++) {
        int personHashCode = new Person().hashCode();
        if (!hashSet.contains(personHashCode)) {
            hashSet.add(personHashCode);
        } else {
            System.out.println("发生了hash冲突,在第" + i + "次,值是:" + personHashCode);
        }
    }
    System.out.println(hashSet.size());
}

通过运行这段代码,可以观察到哈希冲突的发生情况以及发生冲突时的哈希码值,在第105805次发生了hash冲突。

image.png

四、应对哈希冲突的策略

上面问题回答结束之后,面试官或许再问,实际工作中是怎么解决和避免哈希冲突的。

其实哈希冲突是无法避免的,但我们可以采取一些策略来减少其发生的概率和影响:

  1. 优化哈希函数:设计一个更好的哈希函数,使得不同的键更有可能得到不同的哈希值。
  2. 扩大哈希表的大小:通过增加哈希表的大小,可以降低每个存储位置被映射到的概率,从而减少冲突的发生。
  3. 使用链地址法或开放地址法等解决冲突的方法:当发生冲突时,可以通过链表或其他数据结构来存储具有相同哈希值的键值对,或者通过一定的探测方法来寻找下一个可用的存储位置,可以参考hashmap的设计原理。

总之,哈希冲突是哈希表这种数据结构中常见的问题之一,通过深入了解其定义、原因以及应对策略,不仅在工作中可以更好地利用哈希表这一强大的工具来解决实际问题,而且也能在面试中跟面试官侃侃而谈。