指路👉
一、二叉树
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的区别如下:
- JDK1.8之前采用的拉链法,数组+链表
- JDK1.8之后采用数组+链表+红黑树,链表长度大于8且数组长度大于64则会从链表转为红黑树
⭐Q2:HashMap的put方法具体流程
阅读源码
- 判断键值对数组table是否为空或为null,否则执行resizee()进行扩容(初始化)
- 根据键值key计算hash值得到数组索引
- 判断table[i]==null,条件成立,直接新建节点添加
- 如果table[i]==null,不成立
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖
- 判断table[i]是否为treeNode,即table[i]是否为红黑树,如果是红黑树,则直接在树中插入键值对
- 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value
- 插入成功后,判断实际存在的键值对数量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的寻址算法
- 寻址算法
- 计算对象的hashCode()
- 在进行调用hash()方法进行二次哈希,hashcode值右移16位再异或运算,让哈希分布更均匀
- 最后(capacity - 1) & hash得到索引
- 为何HashMap的数组长度一定是2的次幂?
- 计算索引时效率更高:如果是2的n次幂可以使用位与运算代替取模(e.g. 97 % 16 == (16 - 1) & 97)
- 扩容时重新计算索引效率更高: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中死循环的问题