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 函数的更多相关文章

  1. HashMap中的hash函数

    在写一个HashSet时候有个需求,是判断HashSet中是否已经存在对象,存在则取出,不存在则add添加.HashSet也是通过HashMap实现,只用了HashMap的key,value都存储一个 ...

  2. hashCode及HashMap中的hash()函数

    一.hashcode是什么 要理解hashcode首先要理解hash表这个概念 1. 哈希表 hash表也称散列表(Hash table),是根据关键码值(Key value)而直接进行访问的数据结构 ...

  3. [ 转载 ]hashCode及HashMap中的hash()函数

    hashCode及HashMap中的hash()函数   一.hashcode是什么 要理解hashcode首先要理解hash表这个概念 1. 哈希表 hash表也称散列表(Hash table),是 ...

  4. HashMap中的hash算法总结

    前言 算法一直是我的弱项,然而面试中基本是必考的项目,刚好上次看到一个HashMap的面试题,今天也来学习下 HashMap中的hash算法是如何实现的. 数学知识回顾 << : 左移运算 ...

  5. 深入理解HashMap(及hash函数的真正巧妙之处)

    原文地址:http://www.iteye.com/topic/539465 Hashmap是一种非常常用的.应用广泛的数据类型,最近研究到相关的内容,就正好复习一下.网上关于hashmap的文章很多 ...

  6. HashMap中的hash算法中的几个疑问

    HashMap中哈希算法的关键代码 //重新计算哈希值 static final int hash(Object key) { int h; return (key == null) ? 0 : (h ...

  7. 【Java深入研究】11、深入研究hashmap中的hash算法

    一.简介 大家都知道,HashMap中定位到桶的位置 是根据Key的hash值与数组的长度取模来计算的. JDK8中的hash 算法: static final int hash(Object key ...

  8. K:HashMap中hash函数的作用

      在分析了hashCode方法和equals方法之后,我们对hashCode方法和equals方法的相关作用有了大致的了解.在通过查看HashMap类的相关源码的时候,发现其中存在一个int has ...

  9. 【转】【java源码分析】Map中的hash算法分析

    全网把Map中的hash()分析的最透彻的文章,别无二家. 2018年05月09日 09:08:08 阅读数:957 你知道HashMap中hash方法的具体实现吗?你知道HashTable.Conc ...

随机推荐

  1. python爬虫---污言污语网站数据采集

    代码: import requests from lxml import etree headers = { "user-agent": "Mozilla/5.0 (Wi ...

  2. java中的方法(method)到底怎么用?给个例子

    7.方法(method)   被调例子, int add(int x, int y){ return x+y; } 主调例子, for example: int result = add(5,3); ...

  3. Idea导出jar包和使用自定义API

    自定义jar简单实现案例 学习内容 1. 自定义工具类 2. 导出jar 3. 加载Jar包 4. 调用自定义的API方法 总结 学习内容 1. 自定义工具类 新建一个java项目,然后创建包和工具类 ...

  4. Centos搭建 Docker 环境

    搭建 Docker 环境 安装与配置 Docker 安装 Docker Docker 软件包已经包括在默认的 CentOS-Extras 软件源里.因此想要安装 docker,只需要运行下面的 yum ...

  5. FastAPI(六十六)实战开发《在线课程学习系统》接口开发--用户注册接口开发

    在前面我们分析了接口的设计,那么我们现在做接口的开发. 我们先去设计下pydantic用户参数的校验 from pydantic import BaseModel from typing import ...

  6. JWT&RSA实现单点登录(详细介绍)

    今天给大家讲一下基于JWT&RSA的单点登录(Single Sign On,简称SSO)解决方案 概念 首先要了解几个概念 单点登录(Single Sign On) JWT RSA 背景 为什 ...

  7. LC-283

    题目链接:https://leetcode-cn.com/problems/move-zeroes/ 首先想到了快排(简易思想),0为中间点, 把不等于0(注意题目没说不能有负数)的放到中间点的左边, ...

  8. k8s pod故障分类与排查

    一.Pod故障状态基本有几种Pod状态 处于PendingPod状态 处于WaitingPod状态 处于ContainerCreatingPod状态 ImagePullBackOffPod状态 Cra ...

  9. 几个i的幂的累加公式1^2+2^2+3^2 2~5

    1^2+2^2+3^2+--+n^2=n(n+1)(2n+1)/6 1^3+2^3+3^3+...+n^3=(1+2+3+...+n)^2 =[n(n+1)/2]^2 1^4+2^4+3^4+--+n ...

  10. android软件简约记账app开发day10-主页面模块--头信息的展示,和之后功能完善的目标。

    android软件简约记账app开发day10-主页面模块--头信息的展示,和之后功能完善的目标. 今天来写主界面头信息的展示,也就是将第一天的写的layout中的item_main_top展示到主界 ...