转自http://www.cnblogs.com/kaleidoscope/p/9588151.html,感谢作者的无私分享。
转自https://blog.csdn.net/abm1993/article/details/80886058,感谢作者的无私分享。
一、拉链法
一 介绍
拉链法又叫链地址法,Java中的HashMap在存储数据的时候就是用的拉链法来实现的,拉链发就是把具有相同散列地址的关键字(同义词)值放在同一个单链表中,称为同义词链表。有m个散列地址就有m个链表,同时用指针数组T[0..m-1]存放各个链表的头指针,凡是散列地址为i的记录都以结点方式插入到以T[i]为指针的单链表中。T中各分量的初值应为空指针(来自百度知道)。
二 具体实现(依据HashMap分析)
//当我们向HashMap中进行存储数据对象时,我们先根据key中,生成一个hash值
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* @param evict if false, the table is in creation mode.
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0) /*判断当前的HashMap是否已经初始化,
这里的table,是Node<K,V>[] table数组,而Node就是一个链表实现*/
n = (tab = resize()).length;//进行初始化
if ((p = tab[i = (n - 1) & hash]) == null)//位运算获取key对应的数组的索引,并检查对应的数据是否存在
tab[i] = newNode(hash, key, value, null);//当table数组对应没有结点数据时,创建新的结点,此时结点的next为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//jdk1.8之后为了弥补之前数组+链表的实现,
在链表有n个元素时,遍历的时间复杂度O(n),
引入了红黑树(复杂度为O(logn),这里暂不细分,只着重分析拉链法的具体实现*/
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;//循环寻找链表中是否存在同key的情况,存在则进入后续分析
p = e;//既没有达到链表尾部,又没有相同key,继续遍历链表,直到链表尾部
}
}
if (e != null) { // existing mapping for key//后续分析
V oldValue = e.value;//如果进入此判断,说明当前的key值存在值,则更新value值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
后续分析
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//jdk1.8之后为了弥补之前数组+链表的实现,
在链表有n个元素时,遍历的时间复杂度O(n),
引入了红黑树(复杂度为O(logn),这里暂不细分,只着重分析拉链法的具体实现*/
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;//循环寻找链表中是否存在同key的情况,存在则进入后续分析
p = e;//既没有达到链表尾部,又没有相同key,继续遍历链表,直到链表尾部
}
}
if (e != null) { // existing mapping for key//后续分析
V oldValue = e.value;//如果进入此判断,说明当前的key值存在值,则更新value值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
上诉HashMap的添加数据的代码逻辑,淋漓尽致的体现了拉链法的代码实现,简单来说拉链发就是数组加链表
//通过指定的key向HashMap中获取对应的value
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash hash for key
* @param key the key
* @return the node, or null if none
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {//
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)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;//满足条件:HashMap还未初始化,已初始化但length为0,未在HashMap中找到指定key对应的value时返回null
}
后续分析
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//jdk1.8之后为了弥补之前数组+链表的实现,在链表有n个元素时,遍历的时间复杂度O(n),引入了红黑树(复杂度为O(logn),这里暂不细分,只着重分析拉链法的具体实现*/
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;//循环寻找链表中是否存在同key的情况,存在则进入后续分析
p = e;//既没有达到链表尾部,又没有相同key,继续遍历链表,直到链表尾部
}
}
if (e != null) { // existing mapping for key//后续分析
V oldValue = e.value;//如果进入此判断,说明当前的key值存在值,则更新value值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
三 总结
从HashMap的实现来看,我们总结拉链发的实现步骤如下:
1. 计算 key 的 hashValue
2. 根据 hashValue 值定位到 table[hashIndex] 。( table[hashIndex] 是一条链表Node)
3. 若 table[hashValue] 为空则直接插入,不然则添加到链表末尾
与开放定址法相比,拉链法有如下几个优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
拉链法的缺点
指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
使用例子:HashMap
二、开发地址法
开放地址法有个非常关键的特征,就是所有输入的元素全部存放在哈希表里,也就是说,位桶的实现是不需要任何的链表来实现的,换句话说,也就是这个哈希表的装载因子不会超过1。它的实现是在插入一个元素的时候,先通过哈希函数进行判断,若是发生哈希冲突,就以当前地址为基准,根据再寻址的方法(探查序列),去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。所以这种方法又称为再散列法。
有几种常用的探查序列的方法:
①线性探查
dii=1,2,3,…,m-1;这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
(使用例子:ThreadLocal里面的ThreadLocalMap)
②二次探查
di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 );这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
③ 伪随机探测
di=伪随机数序列;具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),生成一个位随机序列,并给定一个随机数做起点,每次去加上这个伪随机数++就可以了。
三、再散列法
再散列法其实很简单,就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就再次散列,直至不发生冲突位置
缺点:每次冲突都要重新散列,计算时间增加。