目录
一、介绍
二、哈希数据结构
三、✍️实现哈希散列
1. 哈希碰撞
2. 拉链寻址⛓️
3. 开放寻址⏩
4. 合并散列
哈希表,也被称为散列表,是一种重要的数据结构。它通过将关键字映射到一个表中的位置来直接访问记录,以此加快查找速度。这种映射函数被称为散列函数。哈希表的历史可以追溯到上个世纪 50 年代,由美国计算机科学家拉宾·珀尔(Rabin Pearl)和罗伯特·韦伯(Robert Weiss)发明。自那时以来,哈希表已经成为了计算机科学和编程中不可或缺的一部分,广泛应用于各种领域。
在计算机中,数据的存储结构主要有两种:数组和链表。数组的优势是长度固定,每个下标都指向唯一的一个值,但同时也存在长度固定的缺点。哈希表则是一种介于数组和链表之间,能够动态调整大小的数据结构。
源码地址:hash_table
说明:通过模拟简单 HashMap 实现,去掉拉链寻址等设计,验证元素索引位置的碰撞。
public class HashMap01 implements Map {
private Logger logger = LoggerFactory.getLogger(HashMap01.class);
private Object[] tab = new Object[8];
@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
tab[idx] = value;
}
@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
return (V) tab[idx];
}
}
测试
@Test
public void test_hashMap01() {
Map map = new HashMap01<>();
map.put("01", "小火龙");
map.put("04", "火爆猴");
logger.info("碰撞前 key:{} value:{}","01",map.get("01"));
// 模拟下标碰撞
map.put("09","可达鸭");
map.put("12","呆呆兽");
logger.info("碰撞后 key:{} value:{}","01",map.get("01"));
}
10:50:36.662 [main] INFO com.pjp.hash_table.test.HashTableTest - 碰撞前 key:01 value:小火龙
10:50:36.666 [main] INFO com.pjp.hash_table.test.HashTableTest - 碰撞后 key:01 value:呆呆兽
说明:既然我们没法控制元素不碰撞,但我们可以对碰撞后的元素进行管理。比如像 HashMap 中拉链法一样,把碰撞的元素存放到链表上。这里我们就来简化实现一下。
public class HashMap02ByZipper implements Map {
private LinkedList>[] tab = new LinkedList[8];
@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new LinkedList<>();
tab[idx].add(new Node<>(key, value));
} else {
tab[idx].add(new Node<>(key, value));
}
}
@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
for (Node kvNode : tab[idx]) {
if (key.equals(kvNode.getKey())) {
return kvNode.getValue();
}
}
return null;
}
static class Node {
final K key;
V value;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
}
}
测试
@Test
public void test_hashMap02() {
Map map = new HashMap02ByZipper<>();
map.put("01", "小火龙");
map.put("04", "火爆猴");
logger.info("碰撞前 key:{} value:{}","01",map.get("01"));
// 模拟下标碰撞
map.put("09","可达鸭");
map.put("12","呆呆兽");
logger.info("碰撞后 key:{} value:{}","01",map.get("01"));
}
12:19:15.505 [main] INFO com.pjp.hash_table.test.HashTableTest - 碰撞前 key:01 value:小火龙
12:19:15.509 [main] INFO com.pjp.hash_table.test.HashTableTest - 碰撞后 key:01 value:小火龙
说明:除了对哈希桶上碰撞的索引元素进行拉链存放,还有不引入新的额外的数据结构,只是在哈希桶上存放碰撞元素的方式。它叫开放寻址,也就是 ThreaLocal 中运用斐波那契散列+开放寻址的处理方式。
public class HashMap03ByOpenAddressing implements Map {
private final Node[] tab = new Node[8];
@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new Node<>(key, value);
} else {
for (int i = idx; i < tab.length; i++) {
if (tab[i] == null) {
tab[i] = new Node<>(key, value);
break;
}
}
}
}
@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
for (int i = idx; i < tab.length; i++) {
// 在开放寻址法中,如果tab[i]为null,则表示该位置没有存储任何元素,因此不需要进行后续的比较操作
if (tab[i] != null && tab[i].key == key) {
return tab[i].value;
}
}
return null;
}
static class Node {
final K key;
V value;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
}
}
测试
@Test
public void test_hashMap03() {
Map map = new HashMap03ByOpenAddressing<>();
map.put("01", "小火龙");
map.put("04", "火爆猴");
logger.info("碰撞前 key:{} value:{}","01",map.get("01"));
// 模拟下标碰撞
map.put("09","可达鸭");
map.put("12","呆呆兽");
logger.info("碰撞后 key:{} value:{}","01",map.get("01"));
}
15:57:33.310 [main] INFO com.pjp.hash_table.test.HashTableTest - 碰撞前 key:01 value:小火龙
15:57:33.313 [main] INFO com.pjp.hash_table.test.HashTableTest - 碰撞后 key:01 value:小火龙
15:57:33.313 [main] INFO com.pjp.hash_table.test.HashTableTest - 数据结构:HashMap{tab=[null,{"key":"01","value":"小火龙"},{"key":"09","value":"可达鸭"},{"key":"12","value":"呆呆兽"},{"key":"04","value":"火爆猴"},null,null,null]}
说明:合并散列是开放寻址和单独链接的混合,碰撞的节点在哈希表中链接。此算法适合固定分配内存的哈希桶,通过存放元素时识别哈希桶上的最大空槽位来解决合并哈希中的冲突。
public class HashMap04ByCoalescedHashing implements Map {
private final Node[] tab = new Node[8];
@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new Node<>(key, value);
}
int cursor = tab.length - 1;
while (tab[cursor] != null && tab[cursor].key != key) {
--cursor;
}
tab[cursor] = new Node<>(key, value);
// 将被碰撞的节点指这个新节点
// while 是为了处理被碰撞节点已经指向了节点,将被碰撞节点指向的节点指向新节点
while (tab[idx].idxOfNext != 0) {
idx = tab[idx].idxOfNext;
}
tab[idx].idxOfNext = cursor;
}
@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
while (tab[idx] != null && tab[idx].key != key) {
idx = tab[idx].idxOfNext;
}
if (tab[idx] == null) {
return null;
}
return tab[idx].value;
}
static class Node {
final K key;
V value;
int idxOfNext;
public Node(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public int getIdxOfNext() {
return idxOfNext;
}
public void setIdxOfNext(int idxOfNext) {
this.idxOfNext = idxOfNext;
}
}
@Override
public String toString() {
return "HashMap{" +
"tab=" + JSON.toJSONString(tab) +
'}';
}
}
同上面测试
15:57:53.650 [main] INFO com.pjp.hash_table.test.HashTableTest - 碰撞前 key:01 value:小火龙
15:57:53.654 [main] INFO com.pjp.hash_table.test.HashTableTest - 碰撞后 key:01 value:小火龙
15:57:53.654 [main] INFO com.pjp.hash_table.test.HashTableTest - 数据结构:HashMap{tab=[null,{"idxOfNext":7,"key":"01","value":"小火龙"},null,{"idxOfNext":0,"key":"12","value":"呆呆兽"},{"idxOfNext":6,"key":"04","value":"火爆猴"},{"idxOfNext":3,"key":"09","value":"可达鸭"},{"idxOfNext":0,"key":"04","value":"火爆猴"},{"idxOfNext":5,"key":"01","value":"小火龙"}]}