哈希表、哈希值计算分析

  • 哈希表完整代码
  • 引出哈希表
  • 哈希表(Hash Table)
  • 哈希冲突(Hash Collision)
  • JDK1.8的哈希冲突解决方案
  • 哈希函数
  • 如何生成 key 的哈希值
    • Integer 的哈希值计算
    • Float 的哈希值计算
    • Long 的哈希值计算
      • 那么, `^` 和 `>>>` 的作用是什么呢?
      • 为什么用 ^ 而不用 &、| 呢?
    • Double 的哈希值计算
    • String 的哈希值计算
    • 自定义对象 的哈希值
  • 自定义对象作为key
    • 重写hashCode方法和equals方法
  • 关于使用%来计算索引
  • 易错点总结

哈希表完整代码

哈希表完整代码

引出哈希表

设计一个写字楼通讯录,存放所有公司的通讯信息

  • 座机号码作为 key(假设座机号码最长是 8 位),公司详情(名称、地址等)作为 value
  • 添加、删除、搜索的时间复杂度要求是 O(1)

我们用这种思路,将座机号码作为索引值(数组索引),公司详情作为数据元素值(数组的值)。

由于数组添加、删除、搜索元素的时间复杂度都是o(1)o(1)o(1),可以达到要求。


但是这种做法有缺点。

  • 空间复杂度非常大
    要保证存储所有元素,所以需要开辟一个非常大的空间。

  • 空间使用率极其低,非常浪费内存空间
    就拿电话号码来说,开辟了100000000大小的空间,但是1~99999999之间有很多数字是索引值是用不到的。

其实 数组companies 就是一个哈希表,典型的 【空间换时间】。

哈希表(Hash Table)

哈希表也叫做散列表(hash有“剁碎”的意思)

哈希表是如何实现高效处理数据的?

put("Jack",666);
put("Rose",777);
put("Kate",888);

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

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

  • 哈希表添加、搜索、删除的流程都是类似的

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

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

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

哈希冲突(Hash Collision)

哈希冲突 也叫做 哈希碰撞

  • 2 个不同的 key,经过哈希函数计算出相同的结果

  • key1 ≠ key2,hash(key1) = hash(key2)

如下图,“Rose” 不等于 “Kate”,但是经过哈希函数算出来的索引确实相同的,此时就产生了冲突。

解决哈希冲突的常见方法

  • 开放定址法(Open Addressing) ✓ 按照一定规则向其他地址探测,直到遇到空桶
      线性探测:往下 一个一个顺序查找,直到遇到空桶
      平方探测:往下按照平方数顺序查找,直到遇到空桶

  • 再哈希法(Re-Hashing) ✓ 设计多个哈希函数

  • 链地址法(Separate Chaining) ✓ 比如通过链表将同一index的元素串起来

JDK1.8的哈希冲突解决方案

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

  • 在添加元素时,可能会由单向链表转为红黑树来存储元素
     比如当哈希表容量 ≥ 64 且 单向链表的节点数量大于 8 时

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

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

思考:这里为什么使用单链表而不是双向链表?

  • 每次都是从头节点开始遍历(依次比较每个元素,若有相同key则替换原本的值,若无相同key,则将新元素添加在链表尾部)

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

哈希函数

◼ 哈希表中哈希函数的实现步骤大概如下

  1. 先生成 key 的哈希值(必须是整数)
  2. 再让 key 的哈希值 跟 数组的大小 进行相关运算,生成一个索引值

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

将数组的长度设计为 2 的幂(2n2^n2n)是因为 2n−12^{n - 1}2n−1 的二进制必然为全 1

10 - 1       = 1            2^1 - 1 = 1
100 - 1     = 11       2^2 - 1 = 3
1000 - 1    = 111      2^3 - 1 = 7
10000 - 1   = 1111     2^4 - 1 = 15
100000 - 1  = 11111        2^5 - 1 = 31

哈希值的二进制与一个全为1的二进制 &(与),结果必然小于等于哈希值,如下图。

◼ 良好的哈希函数,让哈希值更加均匀分布 → 减少哈希冲突次数 → 提升哈希表的性能

如何生成 key 的哈希值

key 的常见种类可能有

  • 整数、浮点数、字符串、自定义对象

  • 不同种类的 key ,哈希值的生成方式不一样,但目标是一致的
      尽量让每个 key 的哈希值是唯一的

      尽量让 key 的所有信息参与运算(即让key的所有数字、内容参与运算)

在Java中,HashMap 的 key 必须实现 hashCode、equals 方法,也允许 key 为 null

Integer 的哈希值计算

整数值当做哈希值

比如 10 的哈希值就是 10

Java 中 Integer.hashCode() 源码:

Float 的哈希值计算

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

Java 中 Float.hashCode() 源码:

Long 的哈希值计算

Long.HashCode() ,Java源码如下:

那么, ^>>> 的作用是什么呢?

  • 高32bit 和 低32bit 混合计算出 32bit 的哈希值

  • 充分利用所有信息计算出哈希值

>>> 即 无符号右移,代码中的 value >>> 32 即将 value 的值无符号右移 32 位。

^ 即位运算中的异或,它的作用是:两数异或,不同为1,相同为0。

为什么用 ^ 而不用 &、| 呢?

  • & 与:结果就是低32位,那高32位相当于没参数运算
  • | 或:如果高32位都是1,那么结果全是1,很容易造成哈希冲突
  • ^ 异:最合适的,把高32位 和 低32位,全部参与运算

>>> 无符号右移:无论正负数高位全部补0
>> 有符号右移:正数高位全部补0,负数高位全部补1


由上图可知,将 64bit 的二进制数字,无符号右移 32bit 后,左边32位必然全为 0(第二行), 右边则为左32位的数字,(value ^ (value >>> 32)) 相当于用自己的右32位 异或 自己的左32位,最后(int)强转回了 32bit ,相当于把左边的32位全部去掉。这样可以充分利用了自身所有信息来计算哈希值。

Double 的哈希值计算

Double.HashCode() 的计算和 Long 差不多

String 的哈希值计算

思考一下:整数 5489 是如何计算出来的?

5∗103+4∗102+8∗101+9∗1005 ∗ 10^3 + 4 ∗ 10^2 + 8 ∗ 10^1 + 9 ∗ 10^05∗103+4∗102+8∗101+9∗100

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

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

  • 因此,jack 的哈希值可以表示为 j∗n3+a∗n2+c∗n2+k∗n0j \ ∗\ n^3 + a\ ∗\ n^2 + c\ ∗\ n^2 + k\ ∗\ n^0j ∗ n3+a ∗ n2+c ∗ n2+k ∗ n0
    等价于[(j∗n+a)∗n+c]∗n+k[(j*n + a) * n + c ] * n + k[(j∗n+a)∗n+c]∗n+k(这样写效率比上面更高)

new String().hashCode() 源码如下:

由上图可知,在JDK中,乘数n为31,为什么使用31?

  • JVM 会将 31 * i 优化成 (i << 5) - i

  • 31∗i=(25−1)∗i=i∗25−i31 * i =(2^5-1) * i = i * 2^5 - i31∗i=(25−1)∗i=i∗25−i

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

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

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

因此在Java中,下面两种写法完全等价。

自定义对象 的哈希值

自定义对象作为key

重写hashCode方法和equals方法

自定义对象作为 key,最好同时重写 hashCode 、equals 方法,什么时候调用?

  • hashCode:计算索引的时候调用,让索引分布均匀,避免哈希冲突,即插入到数组中的哪个位置的时候;

  • equals:当发生哈希冲突,比较两个对象是否相同的时候调用,即在计算完hashCode的之后

equals 用以判断 2 个key 是否为同一个key。

  • 自反性
    对于任何非 null 的 x
    x.equals(x) 必须返回 true

  • 对称性
    对于任何非 null 的 x、y
    如果y.equals(x)返回 true
    x.equals(y)必须返回 true

  • 传递性
    对于任何非 null 的 x、y、z
    如果x.equals(y)y.equals(z)返回 true
    那么x.equals(z)必须返回 true

  • 一致性
    对于任何非 null 的 x、y
    只要 equals 的比较操作在对象中所用的信息没有被修改
    x.equals(y)的结果就不会变

hashCode

  • 必须保证 equals 为 true 的 2 个 key 的哈希值一样

  • 反过来 hashCode 相等的 key,不一定 equals 为 true

◼ 不重写 hashCode 方法只重写 equals 会有什么后果?

✓ 可能会导致 2 个 equals 为 true 的 key 同时存在哈希表中

不同类型的数据有可能计算出相同的哈希值,例如 String类型的数据与Double类型的数据的哈希值可能相同,此时会产生冲突,Java中解决冲突的方法为单链表与红黑树。

当两个 key 不相等,但是哈希值相等时,我们需要用 equals 方法来判断两个 key 是否为同一个key,此时就需要重写 equals。

public class Person {private int age;private float height;private String name;public Person(int age, float height, String name) {super();this.age = age;this.height = height;this.name = name;}@Override/*** 比较两个对象是否相等*/public boolean equals(Object obj) {if(this == obj) return true;if(obj == null || obj.getClass() != getClass()) return false;Person person = (Person)obj;return person.age == age && person.height == height&& person.name==null ? name==null : person.name.equals(name);// 传入name若为空,则当前对象name也必须为空才为 true// 传入name若不为空,则调用equals方法比较即可}@Overridepublic int hashCode() {int hashCode = Integer.hashCode(age);hashCode = hashCode * 31 + Float.hashCode(height);hashCode = hashCode * 31 + (name!=null ? name.hashCode() : 0);return hashCode;}}

验证一下重写了equalshashCode的作用:

  • 由于重写 hashCode,p1、p2 哈希值必然相等,则放入 map 会去比较 key

  • 由于重写 equals,p1、p2 为同一 key,则 p1 会覆盖 p2

public static void main(String[] args) {Person p1 = new Person(18, 1.75f, "jerry");Person p2 = new Person(18, 1.75f, "jerry");// 由于重写 hashCode(),p1、p2哈希值必然相等Map<Object, Object> map = new HashMap<>();map.put(p1, "abc");map.put("test", "bcd");// 由于 p1 与 p2 哈希值相等// 会比较 p1 与 p2 是否为同一key// 由于重写 equals(), p1、p1为同一keymap.put(p2, "ccc"); // 同一 key 则覆盖,map里的元素数量不增加System.out.println(map.size()); // 2
}
  • 若只重写 hashCode
    由于重写 hashCode,p1、p2 哈希值必然相等,则放入 map 会去比较 key
    但是 equals 默认比较地址,p1、p2地址不同,不为同一 key,因此 map.size() 为3

  • 若只重写 equals
    由于没有重写 hashCode,p1、p2 哈希值大概率不相等(有极小可能相等)
    一般情况下,p1、p2哈希值不相等,map.size()值应当为3
    若是真遇到了哈希值相等的情况,由于重写了 equalsmap.size()值为2

结论就是,重写 hashCodeequals 是最稳妥的做法。

关于使用%来计算索引

◼ 如果使用%来计算索引

  • 建议把哈希表的长度设计为素数(质数)

  • 可以大大减小哈希冲突

10%8 = 2     10%7 = 3
20%8 = 4     20%7 = 6
30%8 = 6     30%7 = 2
40%8 = 0     40%7 = 5
50%8 = 2     50%7 = 1
60%8 = 4     60%7 = 4
70%8 = 6     70%7 = 0

◼ 下表格列出了不同数据规模对应的最佳素数,特点如下

  • 每个素数 略小于 前一个素数的 2倍
  • 每个素数尽可能接近2的幂(2n2^n2n)

易错点总结

  • 哈希值相等,根据哈希函数求的索引必然相等

  • 哈希值不相等,根据哈希函数求的索引有可能相等
    原因是hash_code(key) & (table.length - 1)取模运算可能会遇到相等的情况
    可以理解为 2 % 3 = 2,5 % 3 = 2,2 和 3 不相等,%3 的结果却相等

  • HashMap 的 key 必须实现 hashCode、equals 方法,也允许 key 为null

【数据结构】哈希表、哈希值计算分析相关推荐

  1. 数据结构与算法五:哈希表-哈希函数设计原则-哈希冲突解决方案

    一.哈希表的定义: 二.哈希表举例: 哈希函数就是映射关系 三.哈希表应用举例: Leetcode上第387题: 思路:通过s.charAt(i)-'a'将字符串中的字符映射成hash表,出现一次,在 ...

  2. 哈希表 哈希函数 时间_您需要了解的哈希函数

    哈希表 哈希函数 时间 安全从业人员的功能表中有一个工具可以帮助每个人理解,无论他们对计算机进行什么操作:加密哈希函数. 这听起来听起来像是神秘的,技术性的,甚至可能很无聊,但是我对什么是哈希以及它们 ...

  3. 初识哈希表——哈希表是个啥?

    是什么? 哈希表是一种根据输入信息在已有数据库中快速查找匹配对于信息的算法. 为什么需要? 具体应用场景是,比如在图书管理系统中,用户输入一个图书名称,程序需要在整个图书数据库中找到对应的书籍.如果这 ...

  4. 哈希表哈希碰撞解决办法

    哈希表: 散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构. 给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含 ...

  5. Leetcode 146. LRU缓存机制【哈希表 [哈希表存储每个元素在双向链表中的指针]+双向链表】

    文章目录 问题描述 解题报告 实验代码 参考资料 问题描述 运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制.它应该支持以下操作: 获取数据 get 和 写入数据 put . ...

  6. 哈希表+哈希桶简介及实现

    最近在学习通信协议,链路层传输时,对于报文的识别处理,建立哈希识别规则:之前简单学习过哈希,还没深入过,闲来无事把学习的内容通俗的表达出来,也和大家分享一下,让新入门的童鞋很快理解. 1.哈希表 我们 ...

  7. hash:哈希表 哈希桶

    目录 1.哈希的思想 2.解决冲突 3.哈希表(采用的闭散列,线性探测) 4.哈希桶(开散列) 5.总结 1.哈希的思想 hash是hash是一种根据存储的关键字的哈希值确定存储位置的算法.哈希值通过 ...

  8. 哈希表取模选择素数分析

    蝉的哲学 蝉的生命周期为13年或17年,却很少有14.15或16年,为什么呢?蝉是弱势群体,有很多天敌,选择素数作为其生命周期能最大减少与其天敌们共存的时间,增加自己的存活率,这也是自然选择的结果. ...

  9. 【初阶与进阶C++详解】第十九篇:哈希(哈希函数+哈希冲突+哈希表+哈希桶)

最新文章

  1. PHP复习第二天-变量
  2. Distributed Systems笔记-Cryptographic Protocols
  3. python如何计算个人gpa_使用While循环(Python)计算GPA
  4. 安卓开发面试技能介绍,来一份全面的面试宝典练练手,不吃透都对不起自己
  5. 力扣——k个一组翻转链表
  6. [html] 你能否画出一个0.5px的直线?
  7. Zebra项目:分析、实施与测试
  8. 下载文件个别浏览器文件名乱码解决办法
  9. C# 生成pdf文件客户端下载
  10. mysql5.5数据备份_MySql5.5备份和还原
  11. qt android 网络编程实例,【9】QT网络编程
  12. 计算机应用基础案例教程答案,计算机应用基础案例教程问答题答案
  13. java服务器间文件传输,java实现在多服务器之间的文件传输(Jsch)
  14. 如何设计一个应用软件
  15. 微控制器和微处理器的区别(含课本原图)
  16. 2018服务商口碑榜Top50(4月)重磅出炉
  17. TCP/IP Attack Lab(SEED实验)
  18. 关于Git使用详细教程
  19. 开关电源环路的零极点可以在反馈端补偿吗_开关电源的建模和环路补偿设计(1):小信号建模的基本概念和方法(一)...
  20. 【科研工具】【MikTex】MikTex安装和使用

热门文章

  1. torch Dataloader中的num_workers
  2. 金融风控-贷款违约预测
  3. vue实现v-chart绑定数据
  4. 苹果x与苹果xs的区别_苹果Xs相比苹果X,两者谁更值得入手?用户:苹果X宝刀未老!...
  5. conda查找安装包并安装指定版本的安装包
  6. 这款App连夜被下架!
  7. Java必学的工具库,让你的代码量减少90%
  8. 微信小程序开发入门(连载)—— 开发前的准备工作
  9. HTML5+JS游戏开发模块----canvas打字游戏
  10. linux系统实训总结报告,Linux操作系统实验报告.doc