2021-6-11-HashMap面试笔记
Posted 轻舟一曲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2021-6-11-HashMap面试笔记相关的知识,希望对你有一定的参考价值。
HashMap
基础
散列表:数组+链表;
整合了数组快速索引和链表快速插入扩容的特性。
散列表–>哈希
哈希:也称散列,哈希对应的英文都是hash,基本原理就是把任意长度的输入,通过哈希算法变成固定长度的输出。这个压缩映射规则就是对应的哈希算法,而原始的数据映射后的二进制串就是哈希值。
Hash特点:
- 从hash值不可以反向推导hash原始的数据;
- 输入数据的微小变化会得到完全不同的hash值,相同的数据会得到相同的值;
- 哈希算法的执行效率要高效,长的文本也能快速的计算出哈希值;
- hash算法的冲突概率要小;
抽屉原理:一定存在不同的输入映射到相同输出的情况。
散列函数–>散列过程(散列)–>散列表;碰撞;
均匀散列函数:若对于关键字集合中的任一个关键字,经散列函数映象到地址集合中任何一个地址的概率是相等的,这就是使关键字经过散列函数得到一个“随机的地址”,从而减少冲突。
Hash算法也被称为散列算法,Hash算法虽然被称为算法,但实际上它更像是一种思想。Hash算法没有一个固定的公式,只要符合散列思想的算法都可以被称为是Hash算法。
常用散列函数:
- 直接**(线性)**寻址法:H(key)=key或H(key) = a·key + b;
- 数字分析法:找规律找冲突概率小的,比如生日中年月冲突大,月日冲突小;
- 平方取中法:取关键字平方后的中间即为作为散列地址;
- 随机数法:取关键字作为随机函数的种子生成随机值作为散列地址,通常用于关键字长度不同的场合;
- 除留余数法: H(key) = key MOD p,p<=m。对p的选择很重要,一般取素数或m,若p选的不好,容易产生碰撞。
冲突处理策略:
-
开放寻址法:Hi=(H(key) + di) MOD m,i=1,2,…,k(k<=m-1),di为增量序列
- di=1,2,3,…,m-1,称线性探测再散列;
- di=12,-12,22,-22,32,…,**±k2**,(k<=m/2)称二次探测再散列;
- di=伪随机数列,称伪随机探测再散列。
-
再散列法:Hi=RHi(key),i=1,2,…,k RHi均是不同的散列函数;
-
拉链法;
-
建立一个公共溢出区。
原理
继承体系
HashMap是Map接口的非同步实现类
Node
底层数据结构
put
Hash碰撞,链化
JDK8引入红黑树
解决jdk1.7链化严重的问题。
扩容 (核心考点)
数组容量变大,桶位更多,查询效率提高。
源码
-
HashMap除了树化以外,很多地方都进行了优化,尤其是方法都偏向于集中了,而不是各种套娃,估计换了一个很有实力的的团队。
-
HashMap的代码行数是1.7的一倍以上,阅读源码,我们发现HashMap提供了多种转换方式以及内部类,比如keySet,EntrySet,HashIterator等,这里我们要灵活运用
常量分析
树化的另一个参数:当哈希表中的所有元素个数超过64时候,才允许树化。
static final int MIN_TREEIFY_CAPACITY = 64;
哈希表
什么时候初始化?懒加载,第一次赋值的时候才会初始化。
transient Node<K,V>[] table;
//哈希桶的大小
int threshold;
扩容阈值=当前哈希表的大小(初始16)x负载因子(默认0.75)
当哈希表中的元素超过阈值时触发扩容。
JDK1.8 HashMap底层数据结构增加一种红黑二叉树,在极限情况下11条变成红黑二叉树。
11条8(16) => 9(162=>32) => 10(322=>64) => 11(Tree)。
构造方法
4个构造方法。
- 默认构造方式
- 自定义容量构造方式
- 自定义容量,加载因子构造方式
//threshold需要是2的整数次幂
this.threshold = tableSizeFor(initialCapacity);
//将传进的容量的最后一个1到第一个1全部置然后+1就是要扩的容量
static final int tableSizeFor(int cap)
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
- 传入Map构造
put方法
路由寻址公式:(table.length-1)&node.hash
当开始table比较小的时候,很明显hash的高16位是无法参与路由运算的。
解决?
hash:扰动函数
作用:让key的hash值的的高16也参与路由运算。
核心方法:
**putIfAbsent **: true表示如果没有哈希中没有key,就插入,没有就插,一般默认false。
**evict **:表示是否是创建过程,因为Map构造和readObject都是put已经写好的元素了!
//tab:引用当前hashMap的散列表
//p:表示当前散列表的元素
//n:散列表数组的长度
//i:表示路由寻址结果
Node<K,V>[] tab; Node<K,V> p; int n, i;
//延迟初始化逻辑,在第一次调用putval时才会初始化hashmap对象中最消耗内存的散列表
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
第1种情况:寻址找到的桶位刚好是null,这个时候直接讲当前k-v封装成Node丢进去即可
//i=(n - 1) & hash 路由寻址算法:哈希桶大小-1与上经过扰动函数处理得到的哈希值
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
否则的话:
Node<K,V> e; K k;//临时变量
第2中情况:刚好有1个数据
//该桶位元素的键值key刚好与传入的键值key相等并且hash值也一致
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;//找到键值key一致的元素e,用于后序替换
第3种情况:是一颗红黑树
//桶位元素节点已经树化为红黑树节点,此种情况比较复杂在后续讲解
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
第4中情况:是一个链表
else
//在链表上迭代进行比较
for (int binCount = 0; ; ++binCount)
if ((e = p.next) == null) //找到末尾也没有找到key值一致的元素
p.next = newNode(hash, key, value, null);//尾插
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);//链表达到树化条件,进行树化
break;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;//找到key值一致元素e,直接结束后续替换
p = e;
树化的两个条件条件:
-
链表长度>=7 ( binCount >= TREEIFY_THRESHOLD - 1)
-
**且桶数组>=64 **
哈希扩容条件也有两个:
- 哈希容量大于阈值(++size > threshold)
- 树化函数中树化条件不满(哈希桶数组<=64)足也会触发扩容(if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY))
if (e != null) // 存在的话就进行替换
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)//原来的值不存在
e.value = value;
afterNodeAccess(e);
return oldValue;
++modCount;//表示散列表被修改的次数,替换不算
if (c)//哈希容量大于阈值回触发扩容
resize();
afterNodeInsertion(evict);
return null;
①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果 table[i]不为空,转向③;
③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
resize方法(核心)
为什么需要扩容?
为解决哈希冲突导致散列表的链化严重,影响查询效率。
final Node<K,V>[] resize()
//oldTab:引用扩容前的哈希表
Node<K,V>[] oldTab = table;
//oldCap:表示扩容之前table数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//oldThr:表示扩容之前触发扩容的的扩容阈值
int oldThr = threshold;
//扩容之后的table数组大小,下次触发扩容的阈值
int newCap, newThr = 0;
给newCap, newThr这两个变量赋值。
//hashMap中的散列表已经初始化过了,是一次正常的扩容
if (oldCap > 0)
//已经达到最大阈值了
if (oldCap >= MAXIMUM_CAPACITY)
threshold = Integer.MAX_VALUE;//不能再扩容了
return oldTab;
//正常扩容,oldCap << 1,扩大2倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//且扩容前阈值>=16
newThr = oldThr << 1; //阈值也扩大2倍
//oldCap==0的两种情况,散列表未初始化
//1. public HashMap(int initialCapacity, float loadFactor)
//2. public HashMap(int initialCapacity)
//3. public HashMap(Map<? extends K, ? extends V> m) 并且map是有数据的
else if (oldThr > 0) //构造方法中传有一个容量参数
newCap = oldThr;
else //构造方法没有传任何参数
newCap = DEFAULT_INITIAL_CAPACITY;//16
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//12
if (newThr == 0) //一般new的时候传参数比如map和设置容量时候,需要自己设定newThr阈值
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//扩容
table = newTab;
扩容关键方法:
链表节点的扩容:hash&(16-1)=1111 虽然链表的后四位相同,但是再往前不一定相同。
所以再&(32-1)=11111就不一样了。
if (oldTab != null) //扩容之前,table不为null
for (int j = 0; j < oldCap; ++j) //处理桶位中的元素
Node<K,V> e;//处理的当前元素
if ((e = oldTab[j]) != null) //桶位元素不为空
oldTab[j] = null;//置空方便JVM在GC时进行回收内存
//1.单个数据,从未有碰撞
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;//路由选择找到新的索引
//2.已经树化,将红黑树的时候再讲
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//3.桶位已经形成链表
else
//低位链表:存放扩容之后的数组的下标位置与当前数组的下标一致
Node<K,V> loHead = null, loTail = null;
//高位链表:新位置为当前数组下标位置+扩容之前数组的长度
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do
next = e.next;
// hash->...1 1111 高位链中
// hash->...0 1111 低位链中
//oldCap-> 1 0000
if ((e.hash & oldCap) == 0)
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
else
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
while ((e = next) != null);
if (loTail != null) //低位链表有数据
loTail.next = null;
newTab[j] = loHead;
if (hiTail != null) //高位链表有数据
hiTail.next = null;
newTab[j + oldCap] = hiHead;
return newTab;
get方法
final Node<K,V> getNode(int hash, Object key)
//tab:引用当前hashmap的散列表
//first:桶位元素
//e:当前元素
//n:桶数组的长度
//k:当前元素的k值
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
//桶不为空并且所需要的查找(路由寻址)的hash桶位元素不为null
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null)
//桶位元素与查找的元素hash值和key值一致,查找成功
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
return first;
//桶位元素是一个链表节点或已经树化的节点
if ((e = first.next) != null)
//树化节点
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
//链表节点,迭代查找即可
do
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
while ((e = e.next) != null);
return null;
remove方法
key值一致
key值和value值都要一致才能删除
核心方法都是:removeNode
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable)
//tab:引用当前hashMap中散列表
//p:桶位当前元素或者链表的上一元素
//n:表示散列表数组长度
//index:路由寻址索引
Node<K,V>[] tab; Node<K,V> p; int n, index;
//哈希桶数组不为空且路由选址得到的桶位元素不为空
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null)
//node:临时变量存储要删除的节点
//e:当前元素
//k:当前元素的key值
//v:当前元素的value值
Node<K,V> node = null, e; K k; V v;
//桶位元素hash值和key值一致,找到要删除的元素
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null)
//桶位元素已经树化
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
//桶位元素是链表节点,迭代查找
else
do
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k))))
node = e;
break;
p = e;
while ((e = e.next) != null);
//找到的要删除的节点且非空并且判断是否需要value值也一致
if (node != null && (!matchValue || (v [笨叔点滴16]那些狗日的面试必考题(中断管理篇)