黑马程序员Java面试专题(1)|常见集合篇(2)HashMap

指路👉

黑马程序员Java面试专题(1)|常见集合篇(1)ArrayList&LinkedList-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/YOYU_/article/details/135932520

一、二叉树

1.二叉搜索树

通常情况下二叉搜索树的时间复杂度为O(logn),如果是极不平衡的二叉搜索树,会退化为链表,时间复杂度变为O(n)

2.红黑树

  • 红黑树是一种自平衡的二叉搜索树(BST)
  • 所有的红黑规则都是希望红黑树能够保持平衡
  • 红黑树的时间复杂度:查找、添加、删除都是O(logn)

3.散列表(Hash Table)

散列冲突

  • 散列冲突又称哈希冲突,哈希碰撞
  • 指多个key映射到同一个数组下标位置

散列冲突解决--链表法(拉链)

  • 数组的每个下标位置称之为桶(bucket)或者槽(slot)
  • 每个桶(槽)会对应一条链表
  • hash冲突后的元素都放到相同槽位对应的链表中或红黑树中

二、常见面试题

⭐Q1:HashMap的实现原理

  • 底层使用hash表数据结构,即数组 +(链表 | 红黑树)
  • 添加数据时,计算key的值确定元素在数组中的下标
    • key相同则替换
    • key不同则存入链表或红黑树中

               获取数据通过key的hash计算数组下标获取元素

其中,HashMap的jdk1.7和jdk1.8的区别如下:

  1. JDK1.8之前采用的拉链法,数组+链表
  2. JDK1.8之后采用数组+链表+红黑树,链表长度大于8且数组长度大于64则会从链表转为红黑树

⭐Q2:HashMap的put方法具体流程

阅读源码

  1. 判断键值对数组table是否为空或为null,否则执行resizee()进行扩容(初始化)
  2. 根据键值key计算hash值得到数组索引
  3. 判断table[i]==null,条件成立,直接新建节点添加
  4. 如果table[i]==null,不成立
    1. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖
    2. 判断table[i]是否为treeNode,即table[i]是否为红黑树,如果是红黑树,则直接在树中插入键值对
    3. 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value
  5. 插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold(数组长度*0.75),如果超过,进行扩容

Q3:讲一讲HashMap的扩容机制

阅读源码

  • 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到了扩容阈值(数组长度*0.75)

  • 每次扩容的时候,都是扩容之前容量的2倍

  • 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中

    • 没有hash冲突的节点,则直接使用e.hash & (newCap - 1)  ==》(按位与,相当于取模e.hash % newCap)计算新数组的索引位置

    • 如果是红黑树,走红黑树的添加

    • 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上

Q4:讲一讲HashMap的寻址算法

  1. 寻址算法
    1. 计算对象的hashCode()
    2. 在进行调用hash()方法进行二次哈希,hashcode值右移16位再异或运算,让哈希分布更均匀
    3. 最后(capacity - 1) & hash得到索引
  2. 为何HashMap的数组长度一定是2的次幂?
    1. 计算索引时效率更高:如果是2的n次幂可以使用位与运算代替取模(e.g. 97 % 16 == (16 - 1) & 97)
    2. 扩容时重新计算索引效率更高:hash & oldCap == 0的元素留在原来位置,否则新位置 = 旧位置 + oldCap

Q5:HashMap在1.7情况下多线程死循环问题

比如说,现在有两个线程:

线程1:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入

线程2:也读取到hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程2结束

线程1:继续执行的时候就会出现死循环的问题

线程1先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。在JDK8将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了JDK7中死循环的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Buuuleven.(程序媛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值