介绍HashMap之前先介绍一下散列表:
散列表介绍:
1.基本概念
在线性表和树表的查找过程中,记录在表中的位置和记录的关键字没有关系,(我理解为,比如关键1第一放可以放在位置1,但是第二次放却可以放在326的位置,每一次放的位置都可以是不同的)因此在这些表中查找时,需要进行一系列关键字比较。这一类的查找是建立在比较上的,查找的效率取决于比较的次数。
散列函数:一个把查找表中的关键字映射为该关键字对应的地址的函数。映射过程中可能会出现不同关键字映射到一个地址的情况,这种情况叫“冲突”,这几个拥有同样映射地址的关键字称为为“同义词”
散列表:是根据关键字而直接进行访问的数据结构,散列表建立了关键字和存储地址间的一种直接映射关系。查找时间复杂度为O(1)
2. 散列函数的构造方法:
构造散列函数关注的点:
a.散列函数的定义域需要包括所有需要存储的关键字
b.散列函数计算出来的地址应该能够等概率,均匀地分布在整个地址空间,从而减少冲突的发生
c.散列函数应该尽可能的简单,能够在较短的时间内计算出任一关键字的地址
常用散列函数:
(1) 直接定址法:
取某关键字的线性函数作为散列地址。散列函数:
H(key) = a*key+b
适用于关键字分布连续的情况,若分布不连续容易造成空位较多,存储浪费
(2) 除留余数法(最常用):
散列表大小为m,取一个不大于m但是接近于或等于m的质数p作为除数,算每个关键字除以该数的余数。散列函数:
H(key) = key % p
关键:选择好p,使得每一个关键字通过该函数转换后尽量等概率地映射到散列空间上的任一地址。从而尽可能地减少冲突。
HashMap中使用的也是扰动函数+取余法:
hash值计算方法,使用扰动函数h >>> 16可以降低碰撞
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
数组下标计算方法。取余法。一般hashmap的数组长度取2的整数次幂。m除以2的k次幂,相当于把m写成二进制数,右移k位,其中右移之后的值即为商,而右移移出去的值,也就是低k位值即为m%(2^k)的值,即为余数。2^k-1写成二进制为高位为0,低位为k个1的一个值,0000111...1,k个1,此时再与length-1按位与,结果为h的低k位,相当于h%(length-1)。p=length-1。
bucketIndex = indexFor(hash, table.length);
static int indexFor(int h, int length) {
return h & (length-1);
}
(3) 数字分析法:
选取数码分布较为均匀的若干位作为散列地址
(4) 平方取中:
取该关键字的平方数的中间几位作为散列地址
(5) 折叠法:
将关键字分割成位数相同的几部分,然后取这几部分的叠加作为散列地址。
3. 处理冲突的方法:
出现冲突时,为该关键字寻找下一个空的hash地址
a. 开放定址法:
可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放
Hi = (H(key) + di) % m
根据di取值不同,可以分为线性探测法,平方探测法,再散列法和伪随机序列法。
b.拉链法:
此时不像开放定址法去在当前散列表中寻找已存在的空位。而是将所有同义词都存储起来。同义词存储在一个线性链表中, 这个线性链表由其散列地址唯一标识。适用于经常进行插入和删除的情况。
散列表图:由一个数组和若干链表组成
4. 散列表查找及其性能分析
散列表的查找效率取决于三个因素:散列函数,处理冲突的方法和装填因子
装填因子,定义为一个表的装满程度
α = 表中记录数n散列表的长度m=
散列表的平均查找长度依赖于散列表的装填因子α。直观的看,α越大,表示装填的记录越满,发生冲突的可能性就越大,反之发生冲突的可能性就越小。
介绍完了散列表,现在来介绍HashMap
HashMap就是一个散列表。通过计算key的hash来选择存储位置。
HashMap的使用
1. 基础介绍:
a.HashMap是一个散列表,它存储的内容是键值对(key-value)映射
b.继承关系:HashMap继承了AbstractMap,实现了Map,Cloneable,java.io.Serializable接口
c. 是否同步:不同步,线程不安全
d. 其key,value都可以为null。HashMap中的映射不是有序的
2. HashMap中hash表的实现
通过拉链法实现的哈希表,存储结构如下:
Entry:单向链表,
Entry<K,V> implements Map.Entry<K,V>
成员变量: final K key;
V value;
Entry<K,V> next;
int hash;
3. HashMap的属性:
EMPTY_TABLE:Entry[]数组,
size:hashmap元素的个数
threshold:HashMap阈值,用于判断是否需要调整HashMap的容量。
threshold = 容量*加载因子,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量扩容。
loadFactor:加载因子,即空间达到总空间的loadFactor时需要加载,默认是0.75
modCount:用来实现fail-fast机制,修改次数
4. HashMap的方法:
a. put()方法
方法头:public V put(K key,V value)
功能:当key不存在map中时,直接添加键值对,当key存在于map中时,使用新的value代替旧的value。直接添加键值对时,操作次数modCount++。
b. addEntry()方法
方法头:void addEntry(int hash,K key,V value,int bucketIndex)
功能:将新增的key-value键值对存入到map中。
先判断是否需要对map进行扩容,判断size和threshold的大小。如果需要扩容就进行扩容。再通过hash函数计算该key在数组中的位置。再往数组里添加新的key-value键值对。调用createEntry()方法
c. createEntry()方法
方法头:createEntry(int hash,K key,V value,int buketIndex)其中buketIndex为数组下标位置。将k-value存储在new Entry中,并构建链表结构
Entry<K,V> e = table[buketIndex];
table[buketIndex] = new Entry<>(hash,key,value,e); size++;
参考博客:https://blog.csdn.net/strivenoend/article/details/80397825
https://www.cnblogs.com/zhengwang/p/8136164.html
参考书籍:王道组编.2018数据结构考研复习指导[M].电子工业出版社,2017:253-256.