哈希函数实现原理(一)

认识一下31这个神奇的数,

  • 31是一个奇素数(即是奇数又是素数)
  • 31 * i 可以写成(i << 5)- i (JVM可以把31 * i 优化成 (i << 5) - i)
  • 素数和其他数相乘的结果比其他方式更容易产生唯一性,减少哈希冲突
  • 31是经过观测分布结果后的选择

哈希表

  • 哈希表类似数组一样,根据索引去存放值,添加、搜索、删除的都可以达到O(1)的级别,索引的计算非常重要
  • 哈希表是典型的空间换时间应用
  • 哈希函数也称散列函数
  • 哈希表内部的数组元素也可以成为桶
    哈希函数实现原理(一)_第1张图片

哈希冲突

两个不同的key,经过哈希函数的计算可能算出同样的结果
即 key1 ≠ kye2 ,hash(key1) = hash(key2)

  • 解决哈希冲突的常见方法
    • 1、开发定址法(按照一定规则向其他地址探测,比如再冲突的位置上向下一个地址探测,或者移动(i^2, i= 1,2,3…)个位置进行探测)知道有空桶
    • 再哈希法(设计多个哈希函数)
    • 链地址法(将同义索引的元素连接起来,java hash使用的)
      哈希函数实现原理(一)_第2张图片

JDK1.8 哈希冲突解决方法(链表+红黑树)

  • 默认使用单向链表将元素串起来
  • 在添加元素时,可能会由单向链表转为红黑树来存储元素
    • 比如哈希表容量大于等于64且单向链表节点数量大于8时
  • 当红黑树节点数量减少到一定程度时,又会转为单向链表

哈希函数实现原理(一)_第3张图片

哈希函数

  1. 生成key的哈希值(必须是整数)
  2. 让key的哈希值跟数组大小进行相关运算,生成一个索引值
  3. 数组的长度最好设计为(2^n)
  4. 充分利用所有信息进行哈希运算

java中的哈希运算

整数

整数值直接当作哈希值
java源码

public static int hashCode(int value) {
        return value;
}
浮点数

将储存的二进制格式转为整数值
java源码

public static int hashCode(float value) {
        return floatToIntBits(value);
}

将Float二进制格式转为整数

public static int floatToIntBits(float value) {
        int result = floatToRawIntBits(value);
        // Check for NaN based on values of bit fields, maximum
        // exponent and nonzero significand.
        if ( ((result & FloatConsts.EXP_BIT_MASK) ==
              FloatConsts.EXP_BIT_MASK) &&
             (result & FloatConsts.SIGNIF_BIT_MASK) != 0)
            result = 0x7fc00000;
        return result;
}
Long和Double的哈希值

Long hashCode

public static int hashCode(long value) {
        return (int)(value ^ (value >>> 32));
}

Doble hashCode

public static int hashCode(double value) {
        long bits = doubleToLongBits(value);
        return (int)(bits ^ (bits >>> 32));
}

代码大致的意思就是,利用数字的高32位和低32为进行异或(^)运算,

  • 为啥用异或?因为产生的唯一性大一些,用其他运算符产生的唯一性没有那么大
  • 为啥要用低32位和32位?返回的hashCode是一个int类型只占32位,因此要充分利用所有的信息进行运行,
字符串的哈希值

"test"字符串的哈希值是怎么计算的?

  • 先来看整数是怎么计算的
    473 = 4 * 10^2 + 7 * 10 ^1 + 3 * 10^0

对于字符串"test"我们可以看成由t、e、s、t四个字符组成(字符的本质也是一个整数)

  • 因此可以表示为: t * n^3 + e * n ^ 2 + s * n^1 + t * n^0
  • 可以等价为 [ ( t ∗ n + e ) ∗ n + s ] ∗ n + t
  • 在JDK中n取31 像前面说的那样,JVM会自动优化31 * i
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;
}
自定义对象的哈希值

创建一个Student类

public class Student {
    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

用java的HashMap写下测试代码,我们可以观察测试这个学生很明显重复了,但由于java的对象地址的唯一性,导致通过key值计算出来的hash值不一样,导致重复的数据

public static void main(String[] args) {
        Student s1 = new Student("测试", 18);
        Student s2 = new Student("测试", 18);
        Map<Object, Object> map = new HashMap<>();
        map.put(s1, 1);
        map.put(s2, 2);
        System.out.println(s1.hashCode());  //460141958
        System.out.println(s2.hashCode());  //1163157884
        System.out.println(map.size());  //2
}

解决方法,在Student里面重写hashCode方法和equles方法
假如我们的需求是只要这个学生的姓名和年龄是一样的我们就认定这个对象是重复的(可根据自身,尽可能运用更多的信息),

    public boolean equals(Object o) {
        if (this == o) return true;
//        if (!(o instanceof Student)) return false;
        if(o == null || o.getClass() != getClass()) return false;
        Student student = (Student) o;
//        return age == student.age &&
//                Objects.equals(name, student.name);
        //字符串可能为空的情况
        return age == student.age && (student.name == null ?  name == null : student.name.equals(name));
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash = 31 * hash + Integer.hashCode(age);
        hash = 31 * hash + (name == null ? 0 : name.hashCode());
        return hash;
    }
  • 不重写 hashCode 方法只重写 equals 会有什么后果?
    • 就向上面讲的那样,虽然在java内存中对象不是同一个对象,计算出来的hash值也不一样,但两个重复数据的对象可能存放在不同的索引,(索引也可能相同,相同即覆盖)
  • 重写 hashCode 方法不重写 equals 会有什么后果?
    • 也可能导致同时存在两个相同的对象,

你可能感兴趣的:(数据结构,java)