HashMap 中的 hash 函数
1. 什么是 hash 函数
hash 函数,即散列函数,或叫哈希函数。它可以将不定长的输入,通过散列算法转换成一个定长的输出,这个输出就是散列值。需要注意的是,不同的输入通过散列函数,也可能会得到同一个散列值。因此我们不能使用散列函数来获取唯一值。
2. HashMap 为什么要使用 hash 函数
Java 的 HashMap 中使用的是数组 + 链表的结构,但在保存时,一个 K - V 键值对应该被存放到数组的哪个位置?
通常我们都会想到:按照存入顺序存放。但是,按照这种策略,在取值时势必需要遍历整个数组,然后一个个去比较它们的 key 是否相等,这对于性能的损耗无疑是很大的。也许你已经猜到了,解决这个问题的办法就是散列函数。
3. 常见的 hash 算法及冲突的解决
在具体介绍 HashMap 如何使用散列函数之前,先简单介绍一下常见的 hash 算法,以便于你可以更加系统地了解它。
a. 直接定址法:直接以关键字k或者k加上某个常数(k+c)作为哈希地址(H(k)=ak+b)。
b. 数字分析法:提取关键字中取值比较均匀的数字作为哈希地址(如一组出生日期,相较于年-月,月-日的差别要大得多,可以降低冲突概率)
c. 分段叠加法:按照哈希表地址位数将关键字分成位数相等的几部分,其中最后一部分可以比较短。然后将这几部分相加,舍弃最高进位后的结果就是该关键字的哈希地址。
d. 平方取中法:如果关键字各个部分分布都不均匀的话,可以先求出它的平方值,然后按照需求取中间的几位作为哈希地址。
e. 伪随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合。
f. 除留余数法:用关键字k除以某个不大于哈希表长度m的数p,将所得余数作为哈希表地址(H(k)=k%p, p<=m; p一般取m或素数)。
上文已经说到,不同的输入通过散列函数,有可能会得到相同的输出。既然通过不同的输入可以得到相同的输出,那么如果发生冲突了怎么办?比如在 HashMap 中,如果两个不同的 key 计算得出的散列值相同,后来的岂不是会覆盖先来的?不用担心,解决 hash 冲突的方法也是有的,常见的有:
a. 链地址法:将哈希表的每个单元作为链表的头结点,所有哈希地址为 i 的元素构成一个同义词链表。即发生冲突时就把该关键字链在以该单元为头结点的链表的尾部。
b. 开放定址法:即发生冲突时,去寻找下一个空的哈希地址。只要哈希表足够大,总能找到空的哈希地址。
c. 再哈希法:即发生冲突时,由其他的函数再计算一次哈希值。
d. 建立公共溢出区:将哈希表分为基本表和溢出表,发生冲突时,将冲突的元素放入溢出表。
可能你已经注意到,HashMap 就是使用链地址法来解决冲突的(jdk8中采用平衡树来替代链表存储冲突的元素,但hash() 方法原理相同)。数组中的每一个单元都会指向一个链表,如果发生冲突,就将 put 进来的 K- V 插入到链表的尾部。
4. HashMap 是如何使用 hash 函数的
首先,我们来看一下在 HashMap 中,最常用的 put() 和 get() 是怎么使用 hash() 的。以下源码均为 jdk7。
// put()
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
// get()
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
可以看到,HashMap 中都是先使用 hash 函数获取一个 hash 值,然后利用得到的 hash 值和容器容量(table.length)计算对象的存放位置(indexFor() 方法)。我们再详细看一下 hash() 和 indexFor() 两个方法。
static int hash(int h) {
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
通过 put() 方法和 get() 方法,我们可以知道,hash() 方法中的参数 h, h = key.hashCode,hash() 方法对 hashCode 分别无符号右移 (>>>) 7 位和 4 位,再与自身进行异或(^)处理。 这么做的目的是什么?
由于 indexFor() 返回的是 h(hash 值) 与 length - 1(容器容量 - 1) 进行按位与运算的结果,若不进行扰动,即 h = key.hashCode(注意,这里的 h 是 indexFor() 方法的参数,即 hash() 方法的返回值,而非 hash() 方法的参数 h),这将会很容易发生冲突。如下图所示,当低位相同时, h & (length - 1) 结果也会是一样的。即 indexFor() 的计算结果只与 hashCode 的低位相关。
在经过扰动算法后,结果如下:
可以明显看到,计算出来的 hash 值不一样了,即二者不会再发生冲突。这就是为什么 hash() 方法中要使用扰动算法:可以有效降低冲突概率。
既然已经解决了 hash() 的计算问题,那么接下来就是计算索引了。
HashMap 通过 hash 值与 length-1 (容器长度-1)进行取模(%)运算。可能有人会问:明明源码中 indexFor() 方法进行的 按位与(&)运算,而非取模运算。
实际上,HashMap 中的 indexFor() 方法就是在进行取模运算。利用位运算代替取模运算,可以大大提高程序的计算效率。位运算可以直接对内存数据进行操作,不需要转换成十进制,因此效率要高得多。
需要注意的是,只有在特定情况下,位运算才可以转换成取模运算(当 b = 2^n 时,a % b = a & (b - 1) )。也是因此,HashMap 才将初始长度设置为 16,且扩容只能是以 2 的倍数(2^n)扩容。
5. 总结
a. hash 函数并不能保证得到唯一的输出值,不同的输入也有可能得到相同的输出。
b. HashMap 中的 hash() 方法,将 hashCode 的高位和低位混合起来,降低冲突概率。
c. HashMap 中解决冲突的办法是采用链地址法(jdk7)。
d. HashMap 的初始长度为 16,且每次扩容都必须以 2 的倍数(2^n)扩充。因为在 HashMap 中,采用按位与运算(&)代替取模运算(&),当 b = 2^n 时,a % b = a & (b - 1) 。
HashMap 中的 hash 函数的更多相关文章
- HashMap中的hash函数
在写一个HashSet时候有个需求,是判断HashSet中是否已经存在对象,存在则取出,不存在则add添加.HashSet也是通过HashMap实现,只用了HashMap的key,value都存储一个 ...
- hashCode及HashMap中的hash()函数
一.hashcode是什么 要理解hashcode首先要理解hash表这个概念 1. 哈希表 hash表也称散列表(Hash table),是根据关键码值(Key value)而直接进行访问的数据结构 ...
- [ 转载 ]hashCode及HashMap中的hash()函数
hashCode及HashMap中的hash()函数 一.hashcode是什么 要理解hashcode首先要理解hash表这个概念 1. 哈希表 hash表也称散列表(Hash table),是 ...
- HashMap中的hash算法总结
前言 算法一直是我的弱项,然而面试中基本是必考的项目,刚好上次看到一个HashMap的面试题,今天也来学习下 HashMap中的hash算法是如何实现的. 数学知识回顾 << : 左移运算 ...
- 深入理解HashMap(及hash函数的真正巧妙之处)
原文地址:http://www.iteye.com/topic/539465 Hashmap是一种非常常用的.应用广泛的数据类型,最近研究到相关的内容,就正好复习一下.网上关于hashmap的文章很多 ...
- HashMap中的hash算法中的几个疑问
HashMap中哈希算法的关键代码 //重新计算哈希值 static final int hash(Object key) { int h; return (key == null) ? 0 : (h ...
- 【Java深入研究】11、深入研究hashmap中的hash算法
一.简介 大家都知道,HashMap中定位到桶的位置 是根据Key的hash值与数组的长度取模来计算的. JDK8中的hash 算法: static final int hash(Object key ...
- K:HashMap中hash函数的作用
在分析了hashCode方法和equals方法之后,我们对hashCode方法和equals方法的相关作用有了大致的了解.在通过查看HashMap类的相关源码的时候,发现其中存在一个int has ...
- 【转】【java源码分析】Map中的hash算法分析
全网把Map中的hash()分析的最透彻的文章,别无二家. 2018年05月09日 09:08:08 阅读数:957 你知道HashMap中hash方法的具体实现吗?你知道HashTable.Conc ...
随机推荐
- React 日常记录
以下是学习重点 原文地址 浏览器环境 JS解析和执行.绘制.事件处理.静态资源加载和处理 GUI渲染线程和Javascript线程 调度策略 先到先得(FCFS) 对短进程不利 对I/O密集不利 单处 ...
- Android 12(S) 图像显示系统 - 基础知识之 BitTube
必读: Android 12(S) 图像显示系统 - 开篇 一.基本概念 在Android显示子系统中,我们会看到有使用BitTube来进行跨进程数据传递.BitTube的实现很简洁,就是一对&quo ...
- ABP框架入门
技术要求 在开始使用 ABP 框架之前,您需要在计算机上安装一些工具. IDE/编辑器 本书假设您使用的是Visual Studio 2022(支持 .NET 6.0 的 v10.0)或更高版本.如果 ...
- 数据结构篇(2) ts实现单链表
interface NodeItem { prev: NodeItem | null next: NodeItem | null data: any } class NodeItem { prev: ...
- OpenHarmony 3.1 Beta版本关键特性解析——分布式DeviceProfile
(以下内容来自开发者分享,不代表 OpenHarmony 项目群工作委员会观点) 成翔 OpenAtom OpenHarmony(以下简称"OpenHarmony")作为分布式操作 ...
- liunx对磁盘进行配额限制
说明,我们给你一个分区进行磁盘配额进行限制 1.首先我们要进行那个分区进行限制,通过修改配置文件加上uquota ,然后再重启一下系统 二.quota 命令用于管理设备的磁盘容量配额,语法格式为 xf ...
- 【审视】Scrum Master的检查清单
一般情况下,一个Scrum Master如果更多的是做组织会议.确保时间盒以及对流程中的障碍快速响应等事项的话,可以同时引导2-3个团队.在这种情况下,团队会在降低问题发生率的基础上提高一定的绩效. ...
- .NET桌面程序集成Web网页开发的多种解决方案
系列目录 [已更新最新开发文章,点击查看详细] B/S架构的Web程序几乎占据了应用软件的绝大多数市场,但是C/S架构的WinForm.WPF客户端程序依然具有很实用的价值,如设计类软件 Au ...
- Bugku练习题---MISC---蜜雪冰城~
Bugku练习题---MISC---蜜雪冰城~ flag:flag{1251_521_m1xueb1n9chen9ti@nm1mi} 解题步骤: 1.观察题目,下载附件 2.拿到手以后发现有好几个文件 ...
- Dockerfile 中对常用命令详解
说明 Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明. 在Dockerfile 中命令书写对先后顺序及表示其执行对顺序,在书写时需注意. 约定 命令不 ...