哈希表(散列表)

哈希表(散列表)

k-v形式,将k通过hash函数处理生成 table中的索引

添加、搜索、删除的流程都是类似的

利用哈希函数生成 key 对应的 index 【O(1)】

根据 index 操作定位数组元素 【O(1)】

(空间换时间的典型应用)

  • 哈希表是【空间换时间】的典型应用

  • 从哈希函数,也叫做散列函数

  • 哈希表内部的数组元素,很多地方也叫 Bucket(桶),整个数组叫 Buckets 或者 Bucket Array

hash冲突(hash碰撞)

两个不同key通过hash函数计算出相同hash值

解决hash冲突常见方法:

  • 开放地址法

    • 按照一定规则向其他地址探测,直到遇到空桶

  • 再hash法

    • 设计多个hash函数,若冲突则通过其他hash函数再计算hash值

  • 链地址法

    • 通过链表将同一index的元素串起来

jdk8使用单向链表加红黑树解决hash冲突

  • 默认使用单向链表将元素串起来

  • 在添加元素时,可能会由单向链表转为红黑树来存储元素

    • 比如当哈希表容量 ≥ 64 且 单向链表的节点数量大于 8 时

    • 当红黑树节点数量少到一定程度时,又会转为单向链表

  • JDK1.8中的哈希表是使用链表+红黑树解决哈希冲突

思考:这里为什么使用单链表?

  • 每次都是从头节点开始遍历

  • 单向链表比双向链表少一个指针,可以节省内存空间

hash函数

为了提高效率,可以使用 & 位运算取代 % 运算【前提:将数组的长度设计为 2 的幂(2n)

image-20230601154432681

floatToIntBits: 将浮点数底层对应的二进制数转换为整数

toBinaryString:将整数转换为对应的二进制数

Integer.toBinaryString(Float.floatToIntBits(10.6f))

 

java底层hash值计算方式

整数

整数值当做哈希值

比如 10的哈希值就是 10

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

浮点数

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

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

Long和Double

无符号右移 >>>

右移 >>

异或 ^ :相同为0,不同为1

image-20230601155549417

">>>"和"^" 的作用是? 高32bit 和 低32bit 混合计算出 32bit 的哈希值 充分利用所有信息计算出哈希值

image-20230601155622145

(int)(bits ^ (bits >>> 32))

在强制类型装换时,底层会直接将高32位舍弃掉,最后的hash值就是

image-20230601160012824

对应的整数值

字符串

字符串是由若干个字符组成的

比如字符串 jack,由j、a、c、k四个字符组成(字符的本质就是一个整数)

因此,jack 的哈希值可以表示为j *n^3 + a * n^2 + c * n^1 + k * n^0,等价于[(j *n + a) *n+ c] *n+ k

在JDK中,乘数 n 为 31,为什么使用 31?

  • 31 是一个奇素数,JVM会将 31 *i 优化成(i<<5) - i

 

//自己实现: 
public static void main(String[] args) {
       String string = "jack";
       int len = string.length();
       int hashCode = 0;
       for (int i = 0; i < len; i++) {
           char c = string.charAt(i);
           hashCode = hashCode * 31 + c;
        // hashCode = (hashCode << 5) - hashCode + c;
           System.out.println(hashCode);
       // hashCode = ((j* 31 + a)* 31 + c) * 31 + k
      }
  }

//jdk底层 java官方实现:
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;
  }

自定义对象

自定义对象的hash值是根据内存地址生成,所以对于每一次new产生的对象,在java中,若不重写hashCode方法,由于new的对象地址不同,则生成的hash值也不同。

若认为当自定义对象new出来的只要属性值相同,就认为是同一对象时,则就不能使用java默认的使用内存地址生成的hash值(new出来对象内存地址不同),而应该重写hashCode方法根据对象的属性值重新计算hashCode的值。

存入hash表时若hash冲突

存入hash表时若hash冲突,先通过计算出hash值计算出hash表的索引,若索引冲突,

  • 对象:则通过equals(重写equals,不重写又是比较的内存地址)判断对象是否相等(key是否相等)

  • 其他类型: 直接判断key是否相同

重写hashCode与equals

hashCode调用: 计算hash的索引时调用

equals调用: hash冲突时,计算key是否相同调用

额外知识点复习:

getClass与 instanceof

instanceof 用于判断类或者接口的类型时,若是该类的子类,或者接口的实现类,也会认为是同一类型,返回ture

getClass只会判断是不是和该类是同一类型,不会判断其子类和实现类

在计算对象hash值时n为什么选择31

31不仅仅是符合2^n-1,它是个奇素数(既是奇数,又是素数,也就是质数)

  • 素数和其他数相乘的结果比其他方式更容易产成唯一性,减少哈希冲突最终

  • 选择31是经过观测分布结果后的选择

posted @   Boy小康  阅读(77)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· 因为Apifox不支持离线,我果断选择了Apipost!
点击右上角即可分享
微信分享提示