哈希函数基本工作中每天都在使用,独特的数据结构以及复杂的设计原则成为了面试中的重点考点,所以这个系列致力于梳理java中关于hash表的大部分知识点,如有疏漏,欢迎交流,会进行补充。

为了不耽误读者时间,每一篇文章都会列出文章核心知识点。

面试题,收录面试题有时候会将公司列出来,都是收集整理的大厂原题 :

1.Hash是什么?java中你了解哪些Hash表,谈谈(朋友最近去游族面试真题)

2.java中常见类型以及自定义对象的hash函数如何实现?

3.除了链地址法,你还了解哪些处理hash冲突的方法?(网易)

4.为什么HashTable的初始容量设计为11,扩容为何采用原容量*2+1,保持奇数呢?

5.为什么HashMap的容量转变为 2 次幂?

6.为什么要HashMap的hash函数要增加扰动函数,并且位数为16?

7.负载因子为什么是0.75?

8.hashcode和equals方法的作用以及如何重写?

一、哈希函数相关概念

1.hash定义

把任意长度的输入通过散列算法变换成固定长度的输出,该输出就是散列值。

2.hash函数设计原则

良好的hash函数应该是哈希值更加均匀,能够减少哈希冲突次数,提升哈希表的性能。

3.回顾位运算

(1)位运算为什么快?

位运算是汇编级的代码,位运算汇编级执行速度是很快的。

如果只是数值交换,正常情况不用位运算,这点速度提高没意义,而且代码不直观。

口说无凭,写了一段测算代码:

一、测试代码public class BitAndModulusTest {    @Test    public void bit() {        //分别取值10万、100万、1000万        int number = 10000 * 10;        int a = 1;        long start = System.currentTimeMillis();        for(int i = number; i > 0 ; i++) {            a &= i;        }        long end = System.currentTimeMillis();        System.out.println("位运算耗时:" + (end - start));    }    @Test    public void modulus() {        //分别取值10万、100万、1000万        int number = 10000 * 1000;        int a = 1;        long start = System.currentTimeMillis();        for(int i = number; i > 0; i++) {            a %= i;        }        long end = System.currentTimeMillis();        System.out.println("取模运算耗时:" + (end - start));    }}二、测试结果:(时间单位:毫秒)  计算次数     位运算      取模运算    倍数(位运算:取模运算)  10万:       1378      14609    10  100万:       1605      14716    9  1000万:      1637      17669    10三、结论  位运算确实比取模运算快得多,大约快了10倍,当然不同配置机器会有出入。

(2)常见位运算

符号 描述 运算规则
& 同1为1,反之为0
| 有1为1,反之为0
^ 异或 相同为0,反之为1
~ 取反 0变1,1变0
<< 左移 各二进位全部左移若干位,高位丢弃,低位补0
>> 右移 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移)

二、java中常见类型以及对象的hash值

java中的hash值都是是32位的int类型

1.Integer和Short类型hash值就是当前值

//Integer 直接返回当前值public static int hashCode(int value) {    return value;}//Shortpublic static int hashCode(short value) {    return (int)value;}

2.Float类型hash值:将存储的二级制格式转为整数值

浮点数在计算机存储的格式为二级制,hash值是将其该二级制转换为32位整数

(1)Java中如何获得浮点数的hash值以及对应的二进制呢?

//Float.floatToIntBits:将浮点数在计算机中存储的2进制转换为10进制;int hash  = Float.floatToIntBits(1.4f);//Integer.toBinaryString :将10进制转换为2进制String binaryStr = Integer.toBinaryString(hash);System.out.println("hash值:"+hash);System.out.println("二级制字符串:"+binaryStr);输出:hash值:1068708659二级制字符串:111111101100110011001100110011//采用JDK中float的hashcode方法:Float f = new Float(1.4f);System.out.println(f.hashCode());输出:1068708659

3.Long及Double类型hash值:高32位与低32为进行异或运算

由于Long本身就是整数,但它是64位,固只能取其中32位。如果只取前32 或后32 位,并不能保证其高低位的特征,并且容易导致hash值高位或低位相同的不同key发生hash冲突。

问题在于如何最大化的保证其特征,降低冲突。JDK中的做法是将高低位进行异或运算  (HashMap的扰动函数做法相同)

Float类型public static int hashCode(long value) {    //无符号右移再与原值进行异或运算    return (int)(value ^ (value >>> 32));}
Double类型public static int hashCode(double value) {    long bits = doubleToLongBits(value);    return (int)(bits ^ (bits >>> 32));}

计算:

为什么采用异或而不是其他的位运算?

如果用与运算:如果高32位全是1,那么结果就是低32位,无法保证高低位的特征。

同理如果用或运算:若高32位全是1,结果就是高32位,同样无法保证高低位的特征。

但采用异或运算便可以最大化的保证让高低32位都参与运算,保证特征,降低hash冲突。

4.String类型hash值,每一位char都与31相乘并循环累加。

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,31是一个奇素数?

参考这篇文章:

https://www.cnblogs.com/nullllun/p/8350178.html#autoid-3-1-0

大致意思:选择数字31是因为它是一个奇质数,如果选择一个偶数会在乘法运算中产生溢出,导致数值信息丢失,因为乘二相当于移位运算。选择质数的优势并不是特别的明显,但这是一个传统。同时,数字31有一个很好的特性,即乘法运算可以被移位和减法运算取代,来获取更好的性能:31 * i == (i << 5) - i,现代的 Java 虚拟机可以自动的完成这个优化。

5.Boolean:  true为1231,false为1237,并没有什么好说的。

public static int hashCode(boolean value) {    return value ? 1231 : 1237;}

6.Object:与内存地址有关

public native int hashCode();

7.自定义对象hash值

如果没有覆盖hashcode方法,则使用内存地址进行运算获得hash值;

public class Student {    private int age;    private String name;    private float height;    public Student(String name, int age, float height) {        this.age = age;        this.name = name;        this.height = height;    }    @Override    public int hashCode() {        int ageHash = Integer.hashCode(age);        int heightHash = Float.hashCode(height);        int nameHash = (name == null || name == "") ? 0 : name.hashCode();        //利用String的hash值计算的特性;        int result = ageHash * 31 + heightHash;        result = result*31+nameHash;        return result;    }}

三、Hash冲突常见的解决方案

Hash冲突的常见处理方式:

1.链地址法  HashMap,ConcurrentHashMap

比如通过链表将同一个桶位置的元素连接起来

2.开放地址法

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

3.再Hash法  ThreadLocalMap中采用

设计多个hash函数

四、hashcode和equals

hash值一样,一定位于同一个bucket中,用链表;

hash值不一样,有可能与运算得到的索引一样,位于同一个bucket;

hashcode使用:定位索引

equals:索引相同,比较对象是否相同

不实现equals方法,对象默认比较内存地址

不实现hashcode,对象默认比较内存地址

Person p1 = new Person(13,170,"老大");Person p2 = new Person(13,170,"老大");

(1)只实现equals,不实现hashcode

p1和p2的hashcode基于内存计算一定不一样,但他们的hash值对数值与运算后的所有可能一样,可能不一样;

如果索引一样,equals比较后发现是同一个对象,则覆盖;size 为1;

如果索引不一样,则放在不同的bucket;size为2;

(2)只实现hashcode,不实现equals

p1和p2的hash值一样;对数值长度进行与运算后获得索引一样;放在同一个bucket;size=1;

默认equals方法比较内存地址,发现地址不同,p2放在p1后面。size =1;

(3)同时实现hashcode和equals的场景

如果认定对象中成员变量相同就为同一个key,则必须同时实现hashcode和equals方法;

五、Hash表容量的设计原则

Hash表中影响效率的最主要两个参数就是容量以及负载因子,重点探讨这两个参数的设计原则。

Hash表用来保存数据,首先想到的肯定是数据应该被放在哪个桶位,如果定位这个桶。

常规思路:

1.先生成key的hash值(注意java里面hash值都是int类型)

2.让这个hash值与数组长度进行取模,生成索引值

public int getIndex(Object key){    return  hashcode(key)%table.length;}

1.HashTable采用取模运算的数组长度设计

HashTable的特点:

初始容量为11,扩容算法为 原容量*2+1, 保证数组容量始终都是一个素数或奇数。

为什么要这样设计呢?

先说结论:素数或奇数作为hash表的长度,做取模运算能够使得hash分布更加均匀。

2.HashMap容量设计为何转变为2次幂,以及扰动函数的作用?

(1)容量为2的幂次方原因

权威解答参考连接,HashMap的作者Josh Bloch的回答,总结来看即位运算性能较取模运算更高。

如果采用位运算的话数组长度应该如何设计?为什么要设计为2的幂,如何不设置为2的幂会怎样?

下面结合资料以及自身的思考谈谈我的理解:

假设数组长度是15,对应的二级制为:0000 0000 0000 0000 0000 0001 1110key1的hash值假设为1111 1111 1111 1111 1111 1111 1110与15做与运算结果:0000 0000 0000 0000 0000 0000 1110key2的hash值假设为:1111 1111 1111 1111 1111 1111 1111与15做与运算结果:0000 0000 0000 0000 0000 0000 1110结果相同,造成冲突;究其原因无论key的hash是多少,最后一位始终是0,数组的一半空间被浪费。最佳应是每一位都是1,如下形式1111 1111 1111 1111 1111 1111 1111        思考下,这种形式就是  2的幂次方 -10        2^0-1(这种不用考虑,数组容量设置为0干啥)1        2^1-111       2^2-1111      2^3-11111     2^4-111111    2^5-111...11  2^n-1所以将数组的长度设置为2的幂次方后数组的利用率达到最大。

采用位运算的来计算索引,与(2^n-1)做与运算,结果就是自身,且一定小于 2^n-1:

public int getIndex(Object key){    return  hashcode(key)&(table.length-1);}

另外HashMap的容量一般都不会特别大,比如一般不会超过2的16次方,及65536,

假设数组长度为2的10次方,对应的二级制为:0000 0000 0000 0000 0011 1111 1111 1111假设key1的hash值为:1010 0011 0001 1010 1110 1110 0101 0011计算结果:0000 0000 0000 0000 0000 1110 0101 0011可以看到只有最后10位是有效位,参与了运算。这种情况下很多key的hash值后10为可能相同,但是前面22位却可能不同,但他们的索引是相同的,这就造成了冲突。所以需要将key的hash中所有位都能参与运算,最大的保证key的特性。

结论:

1.位运算比求余算法更快。

2.hashcode(key)%table.length 等价于 hashcode(key)&(table.length-1)(数组长度为2次幂)

(2)扰动函数的作用

HashMap使用2的幂作为按位,并且比使用模数更快。但仅仅采用简单hash函数很容易造成hash冲突,原因如下:

key1 = "德玛" 假设其hash值为:0101 1111 0000 1011 0111 0011 1011 0011key2 = "提莫" 假设其hash值为:1100 1011 1010 0000 0100 11111 1011 0011假定数组长度为2的10次方,即1024二级制为:0000 0000 0000 0000 0000 0011 1111 1111计算key1的index:0101 1111 0000 1011 0111 0011 1011 0011 &    0000 0000 0000 0000 0000 0011 1111 1111结果:0000 0000 0000 0000 0000 0011 1011 0011可以看到结果仅仅是保留低10位; 计算key2的index:1100 1011 1010 0000 0100 11111 1011 0011 &    0000 0000 0000 0000 0000 0011 1111 1111结果:0000 0000 0000 0000 0000 0011 1011 0011同样看到结果仅仅是保留低10位;key1和key2的结果是一样的,就造成了hash冲突,原因就在于两个key的高位并没有参与运算,仅仅是保留了低位,所以说模数为2的幂对低位非常敏感。

HashMap增加了扰动函数,让高低16位进行异或运算,最大化的保留了高低位的特征。

static final int hash(Object key) {    int h;    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

为什么要采用异或运算^,而不是与运算&或者或运算| 呢?上面第二节已说明,不再赘述。

六、负载因子的设计原则

该元素表示数组实际存储元素占据数组容量比例,若该比例达到负载因子,则进行扩容。

负载因子多少是合适的呢?

看HashMap和HashTable源码的doc解释,面试的时候回答这些应该够用了,

通常,默认负载因子(.75)在时间和空间成本之间提供了一个很好的权衡。较高的值会减少空间开销,但会增加查找成本(在HashMap类的大多数操作中都得到体现,包括get和put)。设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以最大程度地减少重新哈希操作的次数。如果初始容量大于最大条目数除以负载因子,则将不会进行任何哈希操作。

其实这个数字的选择在其他语言中并不是同一的,比如 Java 是 0.75,Go 中是 0.65,Dart 中是0.8,python 中是0.762,不同场景也会不同,但java设计的思路肯定是保证通用性。

实际上,根据我的计算,“完美”的负载系数更接近对数2(〜0.7)。尽管任何小于此的负载因子都会产生更好的性能。我认为.75可能已被取消。

证明:

通过预测存储桶是否为空,可以避免链接并利用分支预测。如果存储桶为空的可能性超过0.5,则该存储桶可能为空。

让s代表大小,n代表增加的键数。使用二项式定理,存储桶为空的概率为:

P(0) = C(n, 0) * (1/s)^0 * (1 - 1/s)^(n - 0)

因此,如果少于

log(2)/log(s/(s - 1)) keys

当s达到无穷大并且如果添加的键数使得P(0)= .5时,则n / s迅速接近log(2):

lim (log(2)/log(s/(s - 1)))/s as s -> infinity = log(2) ~ 0.693...

七、谈一谈你了解的java中Map实现及异同

JDK中有哪些Map?

HashMap

LinkedHashMap(经常会问到手写LRU)

TreeMap (一致性hash的实现)

WeakHashMap

IdentityHashMap

ThreadLocalMap(ThreadLocal的实现)

ConcurrentHashMap

后面会对这些Map逐一进行比较分析。

引用资料:

1、HashMap为什么需要更好的hashcode算法?

https://www.javaspecialists.eu/archive/Issue054-HashMap-Requires-a-Better-hashCode---JDK-1.4-Part-II.html

2、为什么hash函数采用素数作为容量更好?

https://stackoverflow.com/questions/1145217/why-should-hash-functions-use-a-prime-number-modulus

3、负载因子的取值原因?

https://stackoverflow.com/questions/10901752/what-is-the-significance-of-load-factor-in-hashmap

https://www.javacodegeeks.com/2015/09/an-introduction-to-optimising-a-hashing-strategy.html

float表设计长度_Hash函数设计及面试题分析相关推荐

  1. 使用哈希函数:H(k)=3k MOD 11,并采用链地址法处理冲突。试对关键字序列(22,41,53,46,30,13,01,67)构造哈希表,求等概率情况下查找成功的查找长度,并设计构造哈希表

    使用哈希函数:H(k)=3k MOD 11 ,并采用链地址法处理冲突. 试对关键字序列(22,41,53,46,30,13,01,67)构造哈希表, 求等概率情况下查找成功的查找长度,并设计构造哈希表 ...

  2. java设计一个顺序表类的成员函数_顺序表代码讲解以及实现

    用C语言编写一个有关顺序表的程序代码 创建一个顺序表,其数据元素类型为整型: 在该顺序表中插入数据(#include #include #define MaxSize 50 typedef char ...

  3. 哈希表(哈希函数的设计与哈希冲突的解决)

    文章目录 一.什么是哈希表 二.哈希函数 三.哈希冲突的原因与解决方法 1.数组扩容 2.一个优秀的哈希函数 3.开放寻址法 4.链表法 四.总结 一.什么是哈希表 哈希表就是数组+哈希函数,其核心思 ...

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

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

  5. 有一行文字,具体长度和内容自行约定,设计两个函数:(1)count函数:统计并输出其中英文字母、数字以及其他字符的个数。(2)code函数:译密码,将字符串中的字母按下述规律转换:将字母A变成

    有一行文字,具体长度和内容自行约定,设计两个函数: (1)count函数:统计并输出其中英文字母.数字以及其他字符的个数. (2)code函数:译密码,将字符串中的字母按下述规律转换:将字母A变成字母 ...

  6. c语言自定义函数程序设计,ch3自定义函数设计 C语言 《解析C程序设计》.ppt

    ch3自定义函数设计 C语言 <解析C程序设计> 全局变量--外部变量 在函数外定义的变量 有效范围:从定义变量的位置开始到本源文件结束,及有extern声明的其它源文件 存储类型:缺省e ...

  7. day27 MySQL 表的约束与数据库设计

    day27  MySQL 表的约束与数据库设计 第1节 回顾 1.1  数据库入门 1.1.1 SQL 语句的分类: 1) DDL 数据定义语言 2) DML 数据操作语言 3) DQL 数据查询语言 ...

  8. 【C 语言】C 语言 函数 详解 ( 函数本质 | 顺序点 | 可变参数 | 函数调用 | 函数活动记录 | 函数设计 ) [ C语言核心概念 ]

    相关文章链接 : 1.[嵌入式开发]C语言 指针数组 多维数组 2.[嵌入式开发]C语言 命令行参数 函数指针 gdb调试 3.[嵌入式开发]C语言 结构体相关 的 函数 指针 数组 4.[嵌入式开发 ...

  9. CRC校验原理及CRC-8简单校验函数设计

    CRC校验原理及CRC-8简单校验函数设计 CRC为循环冗余校验码,是一种常用的.具有检错.纠错能力的校验码.通常发送方在发送的数据之后,附上其CRC校验码.接收方收到数据后,也做同样的CRC校验,得 ...

最新文章

  1. 深蓝学院第二章:基于全连接神经网络(FCNN)的手写数字识别
  2. Bootstrap 按钮组
  3. Redis-Session无状态会话技术
  4. MONGODB 集群架构 调整,增加延迟备份节点服务器,删除仲裁节点
  5. mac 完全卸载vscode
  6. 来自网页的消息服务器繁处理忙,EventSource 对象用于接收服务器发送事件通知,是网页自动获取来自服务器的更新...
  7. Hive ROW_NUMBER,RANK(),DENSE_RANK()
  8. jsp网页上实现计算三角形面积小程序
  9. Linux简单基本命令
  10. 若依框架前端Vue项目分析实战
  11. C#: 数字经纬度和度分秒经纬度间的转换
  12. java poi 创建ppt图表,柱状图
  13. 《变量——本土时代的生存策略》(2021-2049)何帆/著 读后感
  14. 广义相对论与狭义相对论的区别
  15. 科大讯飞输入法android离线语音,讯飞输入法Android5.0.1752 离线语音更轻快的表达...
  16. 如何才能降低亚马逊账号关联?
  17. Excel单元格首位数字为“0”不显示的问题
  18. 第一代基因测序信号处理技术
  19. hdu多校第二场 1005 (hdu6595) Everything Is Generated In Equal Probability
  20. ArcGIS计算NDVI为什么只有1和-1及0值

热门文章

  1. Android 7.0+配置Burpsuite证书
  2. 服务器重启之后NVIDIA出现问题原因汇总
  3. Unity中的旋转和矩阵操作
  4. 怎样才能高效的拨打电话—,人工智能系统,呼叫中心,外呼系统建设
  5. EDIUS设置采集磁带的教程
  6. 学习笔记——仅仅为了留下Pima印第安人糖尿病发病数据集的网址
  7. 如何成为一个优秀的综合布线培训讲师
  8. centos7设置键盘类型_CentOS7 安装和部分设置参考
  9. Vue使用指南(一)
  10. 计算机进制各用什么字母表示方法,16进制字母大还是数字大 16进制中的字母代表什么...