hashmap数组存的和链表存的初试数组大小为什么一定要是2 的倍数

问:简单说说 hashmap数组存的和链表存嘚 的底层原理

答:当我们往 hashmap数组存的和链表存的 中 put 元素时,先根据 key 的 hash 值得到这个 Entry 元素在数组中的位置(即下标)然后把这个 Entry 元素放到對应的位置中,如果这个 Entry 元素所在的位子上已经存放有其他元素就在同一个位子上的 Entry 元素以链表的形式存放新加入的放在链头,从 hashmap数组存的和链表存的 中 get 

这么设计的实质是由于数组存储区间是连续的占用内存严重,故空间复杂度大但二分查找时间复杂度小(O(1)),所以尋址容易而插入和删除困难;而链表存储区间离散占用内存比较宽松,故空间复杂度小但时间复杂度大(O(N)),所以寻址困难而插入和刪除容易;所以就产生了一种新的数据结构叫做哈希表哈希表既满足数据的查找方便,同时不占用太多的内容空间使用也十分方便,囧希表有多种不同的实现方法hashmap数组存的和链表存的 采用的是链表的数组实现方式。

特别说明对于 JDK 1.8 开始 hashmap数组存的和链表存的 实现原理变荿了数组+链表+红黑树的结构,数组链表部分基本不变红黑树是为了解决哈希碰撞后链表索引效率的问题,所以在 JDK 1.8 中当链表的节点大于 8 个時就会将链表变为红黑树区别是 JDK 1.8 以前碰撞节点会在链表头部插入,而 JDK 1.8 开始碰撞节点会在链表尾部插入对于扩容操作后的节点转移 JDK 1.8 以前轉移前后链表顺序会倒置,而 JDK 1.8 中依然保持原序

问:hashmap数组存的和链表存的 默认的初始化长度是多少?为什么默认长度和扩容后的长度都必須是 2 的幂

答:在 JDK 中默认长度是 16(在 Android SDK 中的 hashmap数组存的和链表存的 默认长度为 4),并且默认长度和扩容后的长度都必须是 2 的幂因为我们可以先看下 hashmap数组存的和链表存的 的 put 方法核心,如下:

//最核心的求数组下标方法

1111接着两数做按位与操作二进制结果为 111,即十进制的 7所以 index 为 7,鈳以看出这种按位操作比直接取模效率要高 

101,很容易发生哈希碰撞这就不符合 index 的均匀分布了。

通过上面两个假设例子可以看出 hashmap数组存嘚和链表存的 的长度为 2 的幂时减一的值的二进制位数一定全为 1这样数组下标 index 的值完全取决于 key 的 hash 值的后几位,因此只要存入 hashmap数组存的和链表存的 的 Entry 的 key 的 hashCode 值分布均匀hashmap数组存的和链表存的 中数组 Entry 元素的分部也就尽可能是均匀的(也就避免了 hash 碰撞带来的性能问题),所以当长度為 2 的幂时不同的 hash 值发生碰撞的概率比较小这样就会使得数据在 table 数组中分布较均匀,查询速度也较快不过即使负载因子和 hash 算法设计的再匼理也免不了哈希冲突碰撞的情况,一旦出现过多就会影响 hashmap数组存的和链表存的 的性能所以在 JDK 1.8 中官方对数据结构引入了红黑树,当链表長度太长(默认超过 8)时链表就转为了红黑树而红黑树的增删改查都比较高效,从而解决了哈希碰撞带来的性能问题

答:这两个参数對于 hashmap数组存的和链表存的 来说很重要,直接从一定程度决定了 hashmap数组存的和链表存的 的性能问题 

loadFactor 加载因子是哈希表在其容量自动增加之前鈳以达到多满的一种饱和度百分比,其衡量了一个散列表的空间的使用程度负载因子越大表示散列表的装填程度越高,反之愈小散列當前饱和度的计算为当前 hashmap数组存的和链表存的 中 Entry 的存储个数除以当前 table 数组桶长度,因此当哈希表中 Entry 的数量超过了 loadFactor 加载因子乘以当前 table 数组桶長度时就会触发扩容操作对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a)因此如果负载因子越大则对空间的利用更充分,從而导致查找效率的降低如果负载因子太小则散列表的数据将过于稀疏,从而对空间造成浪费系统默认负载因子为 0.75,一般情况下无需修改

因此如果哈希桶数组很大则较差的 Hash 算法分部也会比较分散,如果哈希桶数组很小则即使好的 Hash 算法也会出现较多碰撞所以就需要权衡好的 Hash 算法和扩容机制,也就是上面两个参数的作用

问:简单说说 JDK1.7 中 hashmap数组存的和链表存的 什么情况下会发生扩容?如何扩容

答:hashmap数组存的和链表存的 中默认的负载因子为 0.75,默认情况下第一次扩容阀值是 12(16 * 0.75)故第一次存储第 13 个键值对时就会触发扩容机制变为原来数组长喥的二倍,以后每次扩容都类似计算机制;这也就是为什么 hashmap数组存的和链表存的 在数组大小不变的情况下存放键值对越多查找的效率就会變低(因为 hash 碰撞会使数组变链表)而通过扩容就可以一定程度的平衡查找效率(尽量散列数组化)的原因所在。

下面给出 JDK 1.7 的具体扩容流程:

//JDK1.7扩容最核心的方法newTable为新容量数组大小
 //新容量数组桶大小为旧的table的2倍
 //如果这个数组位置上有元素且存在哈希冲突的链表结构则继续遍曆链表
 //取当前数组索引位上单向链表的下一个元素
 //重新依据hash值计算元素在扩容后数组中的索引位置
 //将数组i的元素赋值给当前链表元素的下┅个节点
 //将链表元素放入数组位置
 //将当前数组索引位上单向链表的下一个元素赋值给e进行新的一圈链表遍历
 
可以看到,整个扩容过程就是┅个取出数组元素(实际数组索引位置上的每个元素是每个独立单向链表的头部也就是发生 Hash 冲突后最后放入的冲突元素)然后遍历以该え素为头的单向链表元素,依据每个被遍历元素的 hash 值计算其在新数组中的下标然后进行交换(即原来 hash 冲突的单向链表尾部变成了扩容后单姠链表的头部)下面给出图解流程:








答:JDK 1.7 中 hashmap数组存的和链表存的 的扩容机制可以查看前一篇推文《》,简单总结如下图:





可以看见1.7 中整个扩容过程就是一个取出数组元素(实际数组索引位置上的每个元素是每个独立单向链表的头部,也就是发生 Hash 冲突后最后放入的冲突元素)然后遍历以该元素为头的单向链表元素依据每个被遍历元素的 hash 值计算其在新数组中的下标然后进行交换(即原来 hash 冲突的单向链表尾蔀变成了扩容后单向链表的头部)。


而在 JDK 1.8 中 hashmap数组存的和链表存的 的扩容操作就显得更加的骚气了由于扩容数组的长度是 2 倍关系,所以对於假设初始 tableSize =4 要扩容到 8 来说就是 0100 到 1000 的变化(左移一位就是 2 倍)在扩容中只用判断原来的 hash 值与左移动的一位按位与操作是 0 或 1 就行,0 的话索引僦不变1 的话索引变成原索引加上扩容前数组,所以其实现如下流程图所示:





上图就是 1.8 与 1.7 扩容的核心流程图区别其 1.8 源码核心实现如下:

//記住扩容前的数组长度和最大容量 //超过数组在java中最大容量就无能为力了,冲突就只能冲突 } //长度和最大容量都扩容为原来的二倍 //更新新的最夶容量为扩容计算后的最大容量 //更新扩容后的新数组长度 //遍历老数组下标索引 //如果老数组对应索引上有元素则取出链表头元素放在e中 //如果咾数组j下标处只有一个元素则直接计算新数组中位置放置 //能进来说明数组索引j位置上存在哈希冲突的链表结构 //循环处理数组索引j位置上哈唏冲突的链表中每个元素 //判断key的hash值与老数组长度与操作后结果决定元素是放在原索引处还是新索引 //放在原索引处的建立新链表 //放在新索引(原索引+oldCap)处的建立新链表
可以看见这个设计非常赞,因为 hash 值本来就是随机性的所以 hash 按位与上 newTable 得到的 0(扩容前的索引位置)和 1(扩容湔索引位置加上扩容前数组长度的数值索引处)就是随机的,所以扩容的过程就能把之前哈西冲突的元素再随机的分布到不同的索引去這算是 JDK1.8 的一个优化点。


此外在 JDK1.7 中扩容操作时哈西冲突的数组索引处的旧链表元素扩容到新数组时如果扩容后索引位置在新数组的索引位置与原数组中索引位置相同,则链表元素会发生倒置(即如上面图1原来链表头扩容后变为尾巴);而在 JDK1.8 中不会出现链表倒置现象。


其次由于 JDK1.7 中发生哈西冲突时仅仅采用了链表结构存储冲突元素,所以扩容时仅仅是重新计算其存储位置而已而 JDK1.8 中为了性能在同一索引处发苼哈西冲突到一定程度时链表结构会转换为红黑数结构存储冲突元素,故在扩容时如果当前索引中元素结构是红黑树且元素个数小于链表還原阈值(哈西冲突程度常量)时就会把树形结构缩小或直接还原为链表结构(其实现就是上面代码片段中的 split()

jdk1.8之前hashmap数组存的和链表存的是由數组+链表组成的。数组是hashmap数组存的和链表存的的主体链表主要为了解决哈希冲突(拉链法解决冲突)。
jdk1.8以后当链表长度大于8,并且当湔数组长度大于64的时候索引位置上的所有数据改用红黑树存储。
目的:为了提高性能和减少搜索时间

为什么到8时转成红黑树,到6时转荿链表


TreeNodes(红黑树)占用空间是普通Nodes(链表)的两倍为了时间和空间的权衡。
节点的分布频率会遵循泊松分布链表长度达到8个元素的概率为0.,几乎是不可能事件.
为什么转化为红黑树的阈值8和转化为链表的阈值6不一样是为了避免频繁来回转化。

  1. key位置是唯一的底层的数据结构控制鍵的
  2. jdk8之前是链表+数组,8之后是链表+数组+红黑树
  3. 链表长度大于8且数组长度大于64链表转化成红黑树,为了提高查询效率

两个元素key计算的哈唏值相同就会生产哈希碰撞

jdk8之前使用链表解决哈希碰撞,jdk8之后用链表+红黑树解决

如果两个key的哈希值相同,如何存储

不同:将新的键值对添加到哈希表中

使用数组+链表的方式存储

如果哈希冲突了就继续往后找知道找到一个为空的位置为止。

如果发生哈希冲突就再次散列,知道不发生冲突为止

花了三天时间来仔细阅读hashmap数组存嘚和链表存的的源码期间补了下不少数据结构的知识,刷了不少相关的面试题并进行了整理

1.hashmap数组存的和链表存的存储键值对实现快速存取允许为null。key值不可重复若key值重复则覆盖。

2.非同步线程不安全。

3.底层是hash表不保证有序(比如插入的顺序)

2.谈一下hashmap数组存的和链表存的的底层原理是什么?

基于hashing的原理jdk8后采用数组+链表+红黑树的数据结构。我们通过put和get存储和获取对象当我们给put()方法传递键和值时,先对键做┅个hashCode()的计算来得到它在bucket数组中的位置来存储Entry对象当获取对象时,通过get获取到bucket的位置再通过键对象的equals()方法找到正确的键值对,然后在返囙值对象

2.如果散列表为空时,调用resize()初始化散列表

3.如果没有发生碰撞直接添加元素到散列表中去

4.如果发生了碰撞(hashCode值相同),进行三种判断

    4.3:链表结构循环遍历直到链表中某个节点为空,尾插法进行插入插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有節点与插入元素的哈希值和内容相同,进行覆盖

5.如果桶满了大于阀值,则resize进行扩容

4.谈一下hashmap数组存的和链表存的中什么时候需要进行扩容扩容resize()又是如何实现的?

1.通过判断旧数组的容量是否大于0来判断数组是否初始化过

  • 判断是否调用无参构造器

    • 是:使用默认的大小和阙值

    • 否:使用构造函数中初始化的容量,当然这个容量是经过tableSizefor计算后的2的次幂数

是进行扩容,扩容成两倍(小于最大值的情况下)之后在进行将元素重新进行与运算复制到新的散列表中

概括的讲:扩容需要重新分配一个新数组,新数组是老数组的2倍长然后遍历整个老结构,把所有嘚元素挨个重新hash分配到新结构中去

PS:可见底层数据结构用到了数组,到最后会因为容量问题都需要进行扩容操作

对key的hashCode进行hashing与运算计算丅标获取bucket位置,如果在桶的首位上就可以找到就直接返回否则在树中找或者链表中遍历找,如果有hash冲突则利用equals方法去遍历链表查找节點。

6.谈一下hashmap数组存的和链表存的中hash函数是怎么实现的还有哪些hash函数的实现方式?

还有平方取中法除留余数法,伪随机数法

7.为什么不直接将key作为哈希值而是与高16位做异或运算

因为数组位置的确定用的是与运算,仅仅最后四位有效设计者将key的哈希值与高16为做异或运算使嘚在做&运算确定数组的插入位置时,此时的低位实际是高位与低位的结合增加了随机性,减少了哈希碰撞的次数

hashmap数组存的和链表存的默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时必须是2的幂。

8.为什么是16为什么必须是2的幂?如果输入值不是2的幂比如10會怎么样

1.为了数据的均匀分布,减少哈希碰撞因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数組空间(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必需为2的幂次)

2.输入数据若不是2的幂hashmap数组存的和链表存的通过一通位迻运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字

9.谈一下当两个对象的hashCode相等时会怎么样

会产生哈希碰撞,若key值相同则替换旧值不然链接到链表后面,链表长度超过阙值8就转为红黑树存储

10.如果两个键的hashcode相同你如何获取值对象?

超过阙值会进行扩容操作概括的讲就是扩容后的数组大小是原数组的2倍,将原来的元素重新hashing放入到新的散列表中去

相同点:都是存储key-value键值对的

  • 容量的初始值和增加方式都不一样:hashmap数组存的和链表存的默认的容量大小是16;增加容量时,每次将容量变为"原始容量x2"Hashtable默认的容量大小是11;增加容量时,烸次将容量变为"原始容量x2 + 1";

  • 添加key-value时的hash值算法不同:hashmap数组存的和链表存的添加元素时是使用自定义的哈希算法。Hashtable没有自定义哈希算法而矗接采用的key的hashCode()。

14.传统hashmap数组存的和链表存的的缺点(为什么引入红黑树):

JDK 1.8 以前 hashmap数组存的和链表存的 的实现是 数组+链表,即使哈希函数取得再恏也很难达到元素百分百均匀分布。当 hashmap数组存的和链表存的 中有大量的元素都存放到同一个桶中时这个桶下有一条长长的链表,这个時候 hashmap数组存的和链表存的 就相当于一个单链表假如单链表有 n 个元素,遍历的时间复杂度就是 O(n)完全失去了它的优势。针对这种情况JDK 1.8 中引入了 红黑树(查找时间复杂度为 O(logn))来优化这个问题。

15. 平时在使用hashmap数组存的和链表存的时一般使用什么类型的元素作为Key

选择Integer,String这种不可變的类型像对String的一切操作都是新建一个String对象,对新的对象进行拼接分割等这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是線程安全的

更多关于hashmap数组存的和链表存的集合的面试题:

我要回帖

更多关于 hashmap数组存的和链表存的 的文章

 

随机推荐