0831(043天 集合框架07 哈希表)
每日一狗(田园犬西瓜瓜)
集合框架07 哈希表
文章目录
1. 哈希表
哈希表:一种以键值对存储数据的结构
1.1 Map<K, V>接口
public interface Map<K, V> {
// k-键 v值 键不可重复
int size(); // 键值对的个数
boolean isEmpty(); // 判定是否包含该 key
V put(K key, V value); // 键值对 放进容器
V remove(Object key); // 删除指定键的
Set<K> keySet(); // 获取键的集合,key不重复
Collection<V> values(); // 获取值的容器,值可重复,无序
}
新增方法
// getOrDefault 获取指定范围的值
/ forEach
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action); // 判定非空
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v); // 调用方法 传入参数
}
}
//replaceAll 对容器中的v进行批量操作
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey(); // 获取键
v = entry.getValue(); // 获取值
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
// ise thrown from function is not a cme.
v = function.apply(k, v); // 调用传入的方法获取返回对象
try {
entry.setValue(v); // 将返回对象重新放回到v中
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
}
}
// putIfAbsent(待整理)
default V putIfAbsent(K key, V value) {
V v = get(key); // 获取键对应的值
if (v == null) {
v = put(key, value); // 值为空时会覆盖数据
}
return v; // 返回 v
}
///remove 按照键值对进行元素的删除,cas
default boolean remove(Object key, Object value) {
Object curValue = get(key);
if (!Objects.equals(curValue, value) || // 存储值和传入值不相同
// 键值对不存在
(curValue == null // 存储的值为空
&& !containsKey(key))) { // 键不存在
return false;
}
remove(key); // 删除元素
return true;
}
replace 和 remove大同小异
boolean replace(K key, V oldValue, V newValue); // 对老数据进行了判定
V replace(K key, V value); // 只对key和value进行了存在判定
1.2 问:一点点重点
hashMap显示顺序不是添加顺序,使用linkedHashMap就有序了
put会在添加数据的同时会将原始存储数据进行返回,但是放入的数据如果和原始存储的数据相同嘞。
V get(Object key);
get时,如果key不存在,会返回null
返回null:没有key;key存储的就是null
V remove(key);
根据键删除,key不存在不会抛出异常,返回值为null
key存在时返回对应的value
遍历map集合的三种方法
- Iterator 迭代器
- forEach 函数式方法
- for(Object tmp : map.keySet())
遍历力度
- map.keySet()
- map.values()
- map.entrySet()
**key是惟一的,需要对元素进行存在性判定时:**存在判定先调用hashCode,相同时在使用equals进行判定。所以一般需要对key中的元素的
hashCode、equals
方法重写。value是可以重复的,在对其进行等值判定只调用equals方法,v中元素只需要对equals进行重写。
1.3 散列算法
Hash一般翻译为散列,也有直接音译为哈希的,这就是把任意长度的输入通过散列算法,变换成固定长度的输出,该输出就是散列值(哈希值);这种转换是一种压缩映射,散列值的空间通常远小于输入的空间,不同的输入可能会散列成相同的输出,所以不可能从散列值来唯一的确定输入值。
所有散列函数都有如下一个基本特性:根据同一散列函数计算出的散列值如果不同,那么输入值肯定也不同。但是,根据同一散列函数计算出的散列值如果相同,输入值不一定相同。
消息摘要算法:md5,SHA-1等
哈希冲突:
不同的输入,相同的输出
解决办法:
开放地址法:你往后存(紧接着的后边)
链地址法:链表,挂到这个地址上的这个链子
以 key/value 的方式存储数据,采用拉链法综合了数组和链表结构。如果key已知则存取效率较高,但是删除慢,如果不知道key存取则慢,对存储空间使用不充分。最典型的实现是HashMap。
1.4 问:思想
数组+链表
哈希表的本质是一个数组,数组中每一个元素称为一个桶,桶中存放的是键值对
存储使用哈希冲突中的链地址法来进行数据存储
链子过长:对链子JDK8+后会使用红黑树进行存储
1.7之前采用Entry数组+链表实现。
1.8采用Node数组+单项链表+红黑树实现,在桶内的节点数量大于8时(并且全部节点数量 > 64 )会转换为红黑树,红黑树的数量小于6时会退化为单向链表
1.7之前采用头插法,会出现环形链,1.8采用尾插法,避免环形链
1.5 实现类
hashMap
负载因子: 负载因子越小哈希碰撞的概率越底,反之节约空间,哈希碰撞概率提升,检索效率底。一般在(0,1】
容积为啥老是2的倍数,因为扩容的时候会扩容两倍,扩容的时候需要移动的数据量就是一半。这样来进行移动代价比较小。
属性
transient int size; // 元素个数
transient int modCount; // 记录修改次数,多线程的操作的快死异常
transient Set<Map.Entry<K,V>> entrySet; // 并不是用于实际存储数据,主要用于针对entrySet和keySet两个视图提供支持
transient Node<K,V>[] table; // 存放数据的数组
/// 重要阈值
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 负载因子:限制当前容器中已经存储的数据量,超过设定及将数组进行扩容
static final int TREEIFY_THRESHOLD = 8;// 树化阈值:即链表转成红黑树的阈值,在存储数据时,当链表长度 > 该值时,则将链表转换成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;// 最小树形化容量阈值:即 当哈希表中的容量 > 该值时,才允许树形化链表(即将链表转换成红黑树)否则,若桶内元素太多时,则直接扩容,而不是树形化
static final int UNTREEIFY_THRESHOLD = 6;// 桶的链表还原阈值:即红黑树转为链表的阈值,当在扩容resize时(此时HashMap的数据存储位置会重新计算),在重新计算存储位置后,当原有的红黑树内数量 < 6时,则将红黑树转换成链表
内部节点实现类
静态内部类用于实现Entry,HahMap中存放的key/value对就被封装为Node对象。其中key就是存放的键值,用于决定具体的存放位置;value是具体存放的数据,hash就是当前Node对象的hash值,next用于指向下一个Node节点(单向链表)
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 节点的哈希值
final K key; // 键
V value; // 值
Node<K,V> next; // 下一个节点
// 单向链表,单链节点数>8(且全链节点数>64时)会进行数组扩容同时转换未红黑树
}
构造器
/// 无参构造,延迟初始化
public HashMap() {
// 延迟初始化 创建数组
// 只是对loadFactor负载因子赋为默认值0.75,
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
/// 指定容积大小,负载因子为默认的0.75
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/// 指定容积、负载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0) // 容积非正判定
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY) // 容积最大值判定
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子合法判定 Float.isNaN 是不是 不是一个浮点数 的判定
if (loadFactor <= 0 || Float.isNaN(loadFactor)) //
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor; //
this.threshold = tableSizeFor(initialCapacity); // 算一个合法的容积来用
}
19 Float.isNaN 是不是 不是一个浮点数 的判定
public static boolean isNaN(float v) {
return (v != v);
}
23 获取合法的初始化容积,比cap刚好大一点的2^n的一个数,,eg:cap=5,return=8
static final int tableSizeFor(int cap) {
int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1); // 将-1向右位运算传入值前边0的个数-1
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
添加元素
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/ 2 hash 计算键对应的哈希值,将高位和低位融合后返回
static final int hash(Object key) {
int h;
return (key == null) ? 0 : // 键不为空
(h = key.hashCode()) // 计算键的hashCode
^ // 高位与底位进行异或运算,让高位和低位共同参与运算
(h >>> 16); // 高位移动到低位
}
// 2 putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, // 容量
i;
// 1. 容器数组位空时进行初始化操作。延迟到这里进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. i = (n - 1) & hash算索引(计算的哈希值对容积进行求余),存储为null时创建node对象插入到对应的桶上
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
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;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
问
问:为什么初始化容积值需要转换位2^n次方
- 扩容代价比较小
- 只有 2^n 次方才能使用位运算进行计算(效率嘎嘎高)
// 求余的实现方法 // 一个数x对2^n进行求余,可以写成 x & (2^n-1) 2%8=2 2 & (8-1) 0010 & 0111 = 0010; 9%8=1 9 & (8-1) 1001 & 0111 = 0001; 15%8=7 15 & (8-1) 1111 & 0111=0111;
问:如何判断 环形链
判定key的重复性:遍历集合,将元素的key存放到set中,如果在下一个key已经在set中存在,则出现环形链表
问:浅谈一下HashMap的构造器
该构造器一共可指定两个参数,int initialCapacity初始化容积(默认DEFAULT_INITIAL_CAPACITY=16), float loadFactor加载因子(默认DEFAULT_LOAD_FACTOR0.75),值越大则存储数据越多,hash冲突的概率越高;值越小则越浪费空间,但是hash冲突的概率越低。
为了减少hash冲突,引入了加载因子,表示了当前容器只能存储最大容积的百分之多少,超过了就会对容器进行扩容。默认容器容积为16,加载因子为0.75,也就是说这个容器在存储到16*0.75=12时,在添加元素就会对容积进行扩容。
在构造器中并没有创建任何用于存储数据的集合—延迟加载,第一次存储数据时才进行空间分配。
扩展小芝士
- 对象的equals方法判定相等,hashCode值必须相等
- 位运算: >>带符号右移(空位置会使用符号位进行补全),>>> 不带符号右移动,空位置使用0进行补全
- 异或运算:不一样才是真