#【2023年面试】
1. hash表的简介
-
核心是哈希桶(数组)和链表
-
O(1)平均查找和删除时间(最理想,最不理想的就是O(n))
-
hash的缺点:在于会出现hash碰撞,没有办法避免,只能尽量避免
-
请问什么叫hash:hash也成散列,基本原理就是把任意长度的输入,通过hash算法变成固定长度的输出,
-
hash的特点:
1.从hash值不可以反向推导出原始数据
2.相同的数据一定会得到相同的hash值,不相同的数据也可能得到相同的hash值
3.hash非常的高效,长的文本也可以很快的算出hash
4.符合抽屉原理,把10个苹果放在九个抽屉里,至少有一个抽屉不少于两个苹果,所以hash冲突不可避免
5.好的hash算法,算出来的hash值尽量分散
2. jdk1.7 hashMap的学习
2.1数据结构
- 经典hash表的实现 数组+链表 ,也叫散列表,整合了数组的索引,整合了链表的动态插入删除,
2.2初始容量
-
初始容量(桶的个数)初始容量其实是空,第一次put值的时候默认用16来作为第一次put值后的初始容量。
-
初始容量必须是2的n次方,如果自定义构造方法初始化的容量不是2的幂的时候,put时,会改写成比自己大的最接近的2的幂
比如,初始容量是7,那么会变成8。
2.3装载因子
- 值为:0.75
- 为什么是0.75,在时间和空间上提供了一个很好的折中!如果高于0.75那么减少了空间的消耗,但是减慢了查找的效率,如果负载因子低于0.75,那么就增加了空间的消耗,但也加快了查找的效率。
- 装载因子的作用:为了算出装载因子
2.4size
- 实际元素数量
2.5hashmap中的常量
// 1. 容量(capacity): HashMap中数组的长度
// a. 容量范围:必须是2的幂 & <最大容量(2的30次方)
// b. 初始容量 = 哈希表创建时的容量
// 默认容量 = 16 = 1<<4 = 00001中的1向左移4位 = 10000 = 十进制的2^4=16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量 = 2的30次方(若传入的容量过大,将被最大值替换)
static final int MAXIMUM_CAPACITY = 1 << 30;
// 2. 加载因子(Load factor):HashMap在其容量自动增加前可达到多满的一种尺度
// a. 加载因子越大、填满的元素越多 = 空间利用率高、但冲突的机会加大、查找效率变低(因为链表变长了)
// b. 加载因子越小、填满的元素越少 = 空间利用率小、冲突的机会减小、查找效率高(链表不长)
// 实际加载因子
final float loadFactor;
// 默认加载因子 = 0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 3. 扩容阈值(threshold):当哈希表的大小 ≥ 扩容阈值时,就会扩容哈希表(即扩充HashMap的容量)
// a. 扩容 = 对哈希表进行resize操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数
// b. 扩容阈值 = 容量 x 加载因子
int threshold;
// 4. 其他
// 存储数据的Entry类型 数组,长度 = 2的幂
// HashMap的实现方式 = 拉链法,Entry数组上的每个元素本质上是一个单向链表
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
// HashMap的大小,即 HashMap中存储的键值对的数量
transient int size;
注意:
- threshold 扩容阈值 = 容量 * 加载因子 不是size * 加载因子。
- 当size>=扩容阈值时,哈希表进行扩容操作。
2.6hashcode如何转化为数组下标
- 答:hash&(length-1) length为容量。
2.7为什么是2的n次方
注意:&按位与的计算方法,链接
- 原因:length 2的n次方无论任何值-1 2进制结果数都是全1值,这样做可以防止2进制出现零的情况,如果二进制出现0,0与任何在一起算都是0,那么就会出现某些桶一直为空情况。
举例:(2-1) ———> 二进制 1
( 4-1) ———> 二进制 11
( 8-1) ———> 二进制 111
(16-1)———> 二进制 1111 - 目的:使计算出的数组下标理论上来说更均匀!
- 注意:最终算出来的index 要满足两个条件1.在0-数组长度之间 2.要平均
2.8hashcode 与hash方法 与indexFor方法
- hash方法是为了尽量减少hash经常碰撞,尽量的分散开来
- indexFor是为了计算出数组下标
- 顺序为:hashcode->hash方法->indexFor方法
2.9扩容
-
resize负责扩容,transfer负责移动元素。
-
每次扩容是原来的容量的两倍。
-
transfer步骤里面的操作是,遍历元素,rehash,计算数组下标。
2.9 hashMap1.7bug1----- 扩容的死锁的问题
- 相关地址:链接
- 原因:多线程操作的情况下,头插法插入入元素
- 有人把这个问题报给了Sun,不过Sun不认为这个是一个问题。因为HashMap本来就不支持并发。要并发就用ConcurrentHashmap
2.10hashmap1.7bug2-----恶意攻击(请求数据为hashcode相同的数据)
- 恶意请求服务器相同的key可造成hash碰撞,也就是某个桶的entry变成链表,查询时间复杂度从O(1)变到O(n)
2.11注意小点
- 为什么扩容不用hashcode%容量-1的方式来计算数组下标~答1.负数对长度求余是负数. 2.计算较慢
3.JDK1.8的实现原理
3.1 JDK1.7与JDK1.8的区别
- 从数组/链表 -> 数组/链表/红黑树
- 扩容时插入顺序的改变,7是头插,8是尾插,虽然尾插不会出现死锁,官方也不建议多线程情况下用hashmap
- 扩容是假如是链表情况,JKD8可以保证链表的顺序不变,JDK7的顺序会倒过来。
- java8引入了函数方法、lamad表达式
3.2红黑树是什么?
- 红黑树是一个可以通过左旋,右旋,变色操作防止变成链表的使自己始终保持接近二叉平衡树(O(logN)的一个数据结构
- 什么是红黑树
- 为什么不一开始就用红黑树而是,从链表变红黑树,因为防止浪费资源,树占的空间大
3.3为什么hashmap单个桶的节点超过8个,就从链表转变成红黑树?
- 官方给的解释是:每个桶的节点数量符合参数为0.5的泊松分布,超过8的概率就非常非常小了
- 注意:是大于8的时候
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
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;
p = e;
}
3.4关键变量
/**
* The default initial capacity - MUST be a power of two.
* 初始容量大小 必须是2的次幂
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
* 最大的容量大小
* 超过这个值就将threshold修改为Integer.MAX_VALUE,数组不进行扩容
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* The load factor used when none specified in constructor.
* 负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
* 链表大于这个值就会树化
* 注意:树化并不是整个map链表,而是某一个大于此阈值的链表
* 树化阈值
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
* 小于这个值就会反树化
* 树变为链表的阈值
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
*树化的另外一个条件 size大于64才可以升级为tree
* */
static final int MIN_TREEIFY_CAPACITY = 64;
/* ---------------- Fields -------------- */
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
* hash表中的数组
*/
transient Node<K,V>[] table;
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* The number of key-value mappings contained in this map.
* 当前hash表中元素个数
*/
transient int size;
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
* hash表修改的计数器
*/
transient int modCount;
/**
* The next size value at which to resize (capacity * load factor).
*
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
当hash表中元素超过这个阈值的时候,触发扩容
int threshold;
/**
* The load factor for the hash table.
*
* @serial
*/
负载因子
final float loadFactor;
3.5四个构造方法
//构造方法1
//指定初始容量大小,负载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) //1<<30 最大容量是 Integer.MAX_VALUE;
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor)