谈谈java中几种常见的散列算法及解决哈希碰撞的方式

由表及里,循序渐进,请往下看。随手点赞是对作者最大的鼓励!^0^。

什么是哈希表

引用:严蔚敏 《数据结构(C语言版)》中的内容 严蔚敏 《数据结构(C语言版)》 截图

哈希表就是 依据关键字可以根据一定的算法(哈希函数)映射到表中的特定位置 的思想建立的表。因此哈希表最大的特点就是可以根据f(K)函数得到其在数组中的索引。

接下来来看看Java中Object对hashCode()方法的说明,当然此方法和equals(Object obj)方法是相辅相成的。

Object类中的equals和hashCode方法(文章内源码均基于JDK8)

equals方法官方文档:

public boolean equals(Object obj)

Indicates whether some other object is "equal to" this one.

The equals method implements an equivalence relation on non-null
object references:

· It is reflexive: for any non-null reference value x, x.equals(x) should return true.
· It is symmetric: for any non-null reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
· It is transitive: for any non-null reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
· It is consistent: for any non-null reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the objects is modified.
· For any non-null reference value x, x.equals(null) should return false.

The equals method for class Object implements the most discriminating possible equivalence relation on objects; that is, for any non-null reference values x and y, this method returns true if and only if x and y refer to the same object (x == y has the value true).

Note that it is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.

Parameters:
obj the reference object with which to compare.
Returns:
true if this object is the same as the obj argument; false otherwise.
See Also:
hashCode(),java.util.HashMap

在官方说明中,指明了equals方法具有自反性、对称性、传递性、一致性,同时也提醒在在继承Object的时候,如果要重写hashCode方法,通常都需要重写该方法,因为hashCode要求(下面也有提及):如果两个对象执行equals方法结果为true,则两对象的哈希码应该是相等的。

hashCode方法官方文档:

public native int hashCode();

Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap.

The general contract of hashCode is:

· Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
· If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.
· It is not required that if two objects are unequal according to the java.lang.Object.equals(java.lang.Object) method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the Java™ programming language.)
Returns:
a hash code value for this object.
See Also:
java.lang.Object.equals(java.lang.Object),java.lang.System.identityHashCode

该方法返回对象的经过处理后的内存地址,由于每个对象的内存地址都不一样,所以哈希码也不一样。此方法为native方法,取决于JVM的内部设计,一般是某种C地址的偏移。

文档中给出了三条规定:

  1. 在对象没有被修改的前提下,执行多次调用,该hashCode方法必须始终返回相同的整数。
  2. 如果两个对象执行equals方法结果为true,则分别调用hashCode方法产生的整数结果是相等的。
  3. 非必要要求:两个对象执行equals方法结果为false,则分别调用hashCode方法产生的整数结果是不相等的。

第三个要求虽然为非必需,但如果实现,则可以提高散列表的性能。

接下来分析几个常见的实现方式。

String的equals和hashCode方法

  • hashCode方法源码:
    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

该函数很简单,以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模,达到了目的——只要字符串的内容相同,返回的哈希码也相同。但是乘子31在此需要解释一下。选31作为乘子,是因为:

  1. 31是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。
  2. 31可以被JVM优化:31 * i = (i << 5) - i。
  • equals方法源码:
    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

此equals方法包含了"==",双等号比较的是地址,存储地址相同,内容则相同。当地址不同的时候,先验证了比较对象是否为String,接着比较了两个字符串的长度,最后才循环比较每个字符是否相等。

Integer的equals和hashCode方法

  • hashCode方法源码:
    @Override
    public int hashCode() {
        return Integer.hashCode(value);
    }
    public static int hashCode(int value) {
        return value;
    }
  • equals方法源码:
     public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

由此可见,Integer哈希码就是Integer对象里所包含的那个整数的数值,且equals方法比较的也是两者的整数数值,即两个数值大小的Integer对象,计算出的哈希码是相等的。

最后,像int,char这样的基础类,它们不需要hashCode,如果需要存储时,将进行自动装箱操作,计算方法同Integer。

哈希碰撞(hash冲突)

在计算hash地址的过程中会出现对于不同的关键字出现相同的哈希地址的情况,即key1 ≠ key2,但是f(key1) = f(key2),这种情况就是Hash 冲突。具有相同关键字的key1和key2称之为同义词。
通过优化哈希函数可以减少这种冲突的情况(如:均衡哈希函数),但是在通用条件下,考虑到于表格的长度有限及关键值(数据)的无限,这种冲突是不可避免的,所以就需要处理冲突。

冲突处理

冲突处理分为以下四种方式:

  1. 开放地址
  2. 再哈希
  3. 链地址
  4. 建立公共溢出区

其中开放地址又分为:

  • 线性探测再散列
  • 二次探测再散列
  • 伪随机探测再散列

下面谈谈几种方法的原理:

开放地址

开放地址法处理冲突的基本原则就是出现冲突后按照一定算法查找一个空位置存放。公式:
clipboard.png
Hi为计算出的地址,H(key)为哈希函数,di为增量。其中di的三种获取方式既是上面提到的开放地址法的三种分类(线性探测再散列、二次探测再散列、伪随机探测再散列)。

  • 线性探测再散列
    clipboard.png
    ,即依次向后查找。
  • 二次探测再散列
    clipboard.png,即依次向前后查找,增量为1、2、3的二次方。
  • 伪随机探测再散列
    伪随机,顾名思义就是随机产生一个增量位移。

再哈希法

再哈希法,就是出现冲突后采用其他的哈希函数计算,直到不再冲突为止。

clipboard.png,其中RHi为不同的哈希函数。

链地址法

链接地址法不同与前两种方法,他是在出现冲突的地方存储一个链表,所有的同义词记录都存在其中。形象点说就行像是在出现冲突的地方直接把后续的值摞上去。例如HashMap,如下图。

clipboard.png

建立公共溢出区

建立公共溢出区的基本思想是:假设哈希函数的值域是[1,m-1],则设向量HashTable[0...m-1]为基本表,每个分量存放一个记录,另外设向量OverTable[0...v]为溢出表,所有关键字和基本表中关键字为同义词的记录,不管它们由哈希函数得到的哈希地址是什么,一旦发生冲突,都填入溢出表。

阅读 11.4k

推荐阅读