哈希表介绍
首先简单介绍一下哈希表的一些基本的概念。
哈希表概念
- 哈希表(Hash table),又称为散列表,通过关键字(key)根据哈希函数(Hash function,散列函数)确定地址而进行直接访问。
- 不同关键字可能散列到同一个地址上,称为冲突(collision)。
- 装载因子(Load factor)为哈希表中的元素个数对该表大小的比。
哈希表的实现
不使用链表实现(例如数组)
关键字输入到哈希函数中确定其在哈希表中的位置,放入表中。如果遇到冲突,解决方法常用的有线性探测法、平方探测法和双散列等。因为Java HashMap使用分离链接法实现,所以这里不详细叙述不使用链表实现。
分离链接法(重点)
将散列到同一个值到所有元素保留在一个表中。新插入的元素在链表前端,不仅仅节省时间,而且最新插入的元素最有可能不久又被访问。
如图所示:
再哈希(rehashing)
当哈希表装载因子到达一定的值,将会尝试扩大哈希表以减少元素冲突,这个过程就是再哈希(又称为再散列)。过程分为两步:
- 扩大表的大小
- 使用新的哈希函数把旧哈希表中的元素重新插入到新哈希表中
HashMap源码解析
Java8之后HashMap的实现分为两种:
- 分离链接法,每个桶(bin)中使用链表结构保存相同hash值的元素;
- 红黑树,每个桶中的所有元素(即相同hash值的元素)使用红黑树结构保存。
分离链接法实现HashMap
在Java8之前HashMap的唯一实现方式。面试可能会问到:
问:在Java8中HashMap有哪些改变?
答:当元素个数增多时,改变为红黑树存储元素。在哈希表容量小于64时使用分离链接法,而超过64并且桶中元素大于8,则对这个桶中的元素使用红黑树进行树化。
源码解析
构造函数
- 函数tableSizeFor(int cap)生成大于等于整型cap的最小的2的幂次结果。函数tableSizeFor详情
- 哈希表使用惰性初始化,构造函数只是设定哈希表参数,而表的空间只有在放入元素时才分配。
static final int MAXIMUM_CAPACITY = 1 << 30;//表的最大容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认装载因子
/*生成一个指定元素容量和装载因子的空哈希表*/
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
/*设置自定的装载因子,和产生一个大于等于n的最小的2的幂次长度的哈希表*/
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);//例如,输入15,则表长度为16
}
/*生成一个默认装载因子,指定容量的空哈希表*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/*生成一个容量为默认16的空哈希表*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/*使用一个map m中的元素初始化新map,装载因子为默认值*/
public HashMap(Map extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
变量的意义
上文提到哈希表使用惰性初始化空间只有在放入元素时才分配,构造函数只是设定哈希表参数,那么哈希表的参数都包括那些呢?
- table数组每个元素位置相当于一个桶;
- 哈希表的大小必须是2的幂次。Java HashMap表的大小为什么必须是2的幂次
transient Node[] table;//表使用node数组表示,数组每个元素相当于一个桶,相同哈希值的元素放入同一个桶中
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//默认表的容量,具体就是table数组的默认大小
static final int TREEIFY_THRESHOLD = 8;//树化时桶中最小的元素容量
static final int UNTREEIFY_THRESHOLD = 6;//如果桶中元素容量低于这个值,退化到链表
static final int MIN_TREEIFY_CAPACITY = 64;//表的容量只有达到64,各个桶才有机会进行树化,否则扩容
transient int size;//哈希表中元素个数
int threshold;//哈希表中元素个数超过threshold则扩容
final float loadFactor;//装载因子
transient int modCount;//修改次数
HashMap的Node结点
注意:
- 结点的key是final修饰的,具有不可变性,为减少对key计算哈希值所消耗的时间,使用变量hash记录key的哈希值;
- 结点重载自己的hashCode和equals函数
static class Node implements Map.Entry {
/*结点包含四个变量,分别是key、key的哈希值、value以及链表中指向后继的指针*/
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
/*考虑到key和value两个变量*/
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
/*key和value均相等则是相等的结点*/
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry,?> e = (Map.Entry,?>)o;
if (Objects.equals(key, e.getKey()) && Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
增删改查基本操作
- 增加操作
/*使用已存在的map赋值*/
final void putMapEntries(Map extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // 惰性初始化
float ft = ((float)s / loadFactor) + 1.0F;//根据数据量和负载因子计算哈希表的大小
int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);//拓展为大于等于的t最小的2的幂次方,暂时设定threshold为这个值
}
else if (s > threshold)//如果元素值超过threshold扩容
resize();
for (Map.Entry extends K, ? extends V> e : m.entrySet()) {//循环遍历
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);//执行每个结点的插入操作
}
}
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node[] tab; Node p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//如果没有初始化table,使用resize函数初始化
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)//如果i位置没有数据,直接放置在i位置
tab[i] = newNode(hash, key, value, null);
else {
Node e; K k;
//如果i位置与插入的相等,这个结点可能是链表的第一个结点,也可能是红黑树的根结点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)//如果是红黑树,执行树的插入
e = ((TreeNode)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);//插入链表的结尾
// 如果链表的结点数超出TREEIFY_THRESH进行树化
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
//如果结点存在
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { //原来存在key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)//存在的情况下也插入,更新value
e.value = value;
afterNodeAccess(e);;//函数在HashMap中无执行逻辑,是为兼容LinkedHashMap的方法
return oldValue;
}
}
++modCount;
if (++size > threshold)//元素个数超过阈值扩容
resize();
afterNodeInsertion(evict);//函数在HashMap中无执行逻辑,是为兼容LinkedHashMap的方法
return null;
}
public void putAll(Map extends K, ? extends V> m) {//对外接口,把m中元素插入当前表中
putMapEntries(m, true);
}
@Override
public V putIfAbsent(K key, V value) {//对外接口,如果不存在插入
return putVal(hash(key), key, value, true, true);
}
public V put(K key, V value) {//对外接口,如果不存在插入新的结点,如果存在更新value值
return putVal(hash(key), key, value, false, true);
}
- 扩容操作
- 切记MAXIMUM_CAPACITY是哈希表数组table的最大长度,数组table的长度一定是2的幂次方。当等于MAXIMUM_CAPACITY时,threshold为整型最大值;否则均扩张为原来的2倍。
- 在HashMap实现中,仅仅保留装载因子loadFactor和最大元素个数的阈值threshold。所以如果数组table尚未分配空间,此时的capacity记录在threshold中。
- 因为新表要么不扩容,要么扩容为原来的2倍大小。旧表的元素插入新表时,同一桶中的结点可能位置是放入与原来相同的索引位置j或者位置j+oldCap,其实只需要区分哈希值中oldCap的二进制表示1所在的那位是0还是1,如果为0放入低位,因为哈希值与newCap-1做与操作后,这一位仍旧为0;1放入高位,哈希值与newCap-1做与操作这一位变为1。
final Node[] resize() {
Node[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//旧表容量大于0
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {//旧表容量超过最大容量,不可以扩容
threshold = Integer.MAX_VALUE;//新表的元素个数阈值为最大整型值
return oldTab;
}
/*新表扩容为旧表容量的2倍*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
/*旧表为null,那么初始化的容量是存储在变量threshold。在HashMap实现中*/
/*在HashMap实现中,仅仅保留装载因子loadFactor和最大元素个数的阈值threshold*/
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {//如果新表的元素个数阈值没有被设置
float ft = (float)newCap * loadFactor;//计算阈值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;//设置新表的阈值
@SuppressWarnings({"rawtypes","unchecked"})
Node[] newTab = (Node[])new Node[newCap];
table = newTab;//初始化新表
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//红黑树执行分裂操作
((TreeNode)e).split(this, newTab, j, oldCap);
else { // 保持原有顺序,把原来链表分裂成两个链表
Node loHead = null, loTail = null;
Node hiHead = null, hiTail = null;
Node next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {//低位链表放置在原来的索引j位置
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {//高位链表放置在索引j+oldCap的位置
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
- 查询操作
final Node getNode(int hash, Object key) {
Node[] tab; Node first, e; int n; K k;
/*hash值定位所在位置*/
if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))//先判断第一个结点
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)//如果是红黑树,执行树的查找
return ((TreeNode)first).getTreeNode(hash, key);
/*链表遍历桶中所有结点*/
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
public V get(Object key) {//查询key的结点,如果存在返回其value,否则返回null
Node e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
@Override
//Java8新增特性
public V getOrDefault(Object key, V defaultValue) {//查询key的结点,如果存在返回其value,否则返回默认值
Node e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
public boolean containsKey(Object key) {//判断是否有key的结点
return getNode(hash(key), key) != null;
}
/*查找value值是否存在*/
public boolean containsValue(Object value) {
Node[] tab; V v;
if ((tab = table) != null && size > 0) {
//遍历表中所有结点
//因此查询value操作相当费时,时间复杂度为O(n),n为表中元素个数
for (int i = 0; i < tab.length; ++i) {
for (Node e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value || (value != null && value.equals(v)))
return true;
}
}
}
return false;
}
- 修改操作
Java8引入的新特性和新功能
@Override
/*为key-oldValue对的结点进行赋值,新值为newValue,key不变*/
public boolean replace(K key, V oldValue, V newValue) {
Node e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
@Override
/*键为key的结点,赋值value*/
public V replace(K key, V value) {
Node e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
@Override
/*对表中的每个结点执行function功能*/
/*红黑树表示的结点同样具有next指针*/
/*红黑树结点不仅仅具有常规的left和right指针,同时具有prev指针、next指针以及parent指针*/
public void replaceAll(BiFunction super K, ? super V, ? extends V> function) {
Node[] tab;
if (function == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node e = tab[i]; e != null; e = e.next) {
e.value = function.apply(e.key, e.value);
}
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
- 删除操作
public V remove(Object key) {
Node e;
return (e = removeNode(hash(key), key, null, false, true)) == null ? null : e.value;
}
@Override
/*Java8引入的新操作,当key-value均匹配是才删除结点*/
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
final Node removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node[] tab; Node p; int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node node = null, e; K k; V v;
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))//第一个结点
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)//树结点,执行树搜索操作
node = ((TreeNode)p).getTreeNode(hash, key);
else {//遍历链表,e为查询的当前结点,p为e的前驱结点
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
/*node记录查询到的结点*/
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)//红黑树执行删除操作
((TreeNode)node).removeTreeNode(this, tab, movable);
else if (node == p)//如果是第一个结点匹配成功
tab[index] = node.next;//桶指向第一个结点的后继结点
else
p.next = node.next;//删除node结点
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
拷贝函数
执行一个浅拷贝。返回一个表的拷贝,但是表中结点的key和value本身没有被拷贝。
@SuppressWarnings("unchecked")
@Override
public Object clone() {
HashMap result;
try {
result = (HashMap)super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false);
return result;
}
Java8新增函数
@Override
/*如果不存在key对应的value,则执行mappingFunction产生value值放入表中*/
public V computeIfAbsent(K key, Function super K, ? extends V> mappingFunction) {
if (mappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node[] tab; Node first; int n, i;
int binCount = 0;
TreeNode t = null;
Node old = null;
if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/*判断key是否存在于表中*/
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode)first).getTreeNode(hash, key);
else {
Node e = first; K k;
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
V oldValue;
/*如果存在value,不做任何操作*/
if (old != null && (oldValue = old.value) != null) {
afterNodeAccess(old);
return oldValue;
}
}
/*产生新的value值*/
V v = mappingFunction.apply(key);
if (v == null) {
return null;
} else if (old != null) {
/*key之前不存在对应的value*/
old.value = v;
afterNodeAccess(old);
return v;
} else if (t != null)
/*红黑树结构*/
t.putTreeVal(this, tab, hash, key, v);
else {
/*插入链表第一个结点位置*/
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
return v;
}
/*存在key结点执行remappingFunction操作*/
public V computeIfPresent(K key, BiFunction super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
Node e; V oldValue;
int hash = hash(key);
//找到键为key的结点
if ((e = getNode(hash, key)) != null && (oldValue = e.value) != null) {
//key-oldValue对进行remappingFunction操作,产生新的value
V v = remappingFunction.apply(key, oldValue);
//新value不为null,覆盖原有的oldValue
if (v != null) {
e.value = v;
afterNodeAccess(e);
return v;
}
//新value为null,删除key的结点
else
removeNode(hash, key, null, false, true);
}
return null;
}
@Override
/*computeIfPresent和computeIfAbsent功能的结合*/
public V compute(K key, BiFunction super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node[] tab; Node first; int n, i;
int binCount = 0;
TreeNode t = null;
Node old = null;
if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/*查看key的结点*/
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode)first).getTreeNode(hash, key);
else {
Node e = first; K k;
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
/*找到产生新的value*/
V oldValue = (old == null) ? null : old.value;
V v = remappingFunction.apply(key, oldValue);
/*存在*/
if (old != null) {
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
}
/*不存在*/
else if (v != null) {
/*红黑树*/
if (t != null)
t.putTreeVal(this, tab, hash, key, v);
else {
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return v;
}
@Override
public V merge(K key, V value, BiFunction super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node[] tab; Node first; int n, i;
int binCount = 0;
TreeNode t = null;
Node old = null;
if (size > threshold || (tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/*查找key对应的结点*/
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode)first).getTreeNode(hash, key);
else {
Node e = first; K k;
do {
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
/*存在的情况*/
if (old != null) {
V v;
/*新旧value值执行remappingFunction操作返回结果,如果旧value为null,结果是新value*/
if (old.value != null)
v = remappingFunction.apply(old.value, value);
else
v = value;
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
return v;
}
/*不存在的情况,插入到表中*/
if (value != null) {
if (t != null)
t.putTreeVal(this, tab, hash, key, value);
else {
tab[i] = newNode(hash, key, value, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return value;
}
@Override
/*表中每个结点的key和value执行action操作*/
public void forEach(BiConsumer super K, ? super V> action) {
Node[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
红黑树实现HashMap
Java8 HashMap的红黑树实现
HashMap的迭代器和EntrySet实现
Java8 HashMap的迭代器和转化Set的实现
相关算法例题
数组中找到两数之和为给定目标值
无重复字符的最长子串