Hash碰撞(Hash Collision)是指在使用哈希函数时发生的一种情况,即两个不同的输入(例如不同的字符串或文件)经过同一个哈希函数处理后产生了相同的哈希值
Hash碰撞就是两个key经过一系列操作得到了相同的值
这篇文章能带你带来什么:
公共pojo
public class Node<K,V> implements Map.Entry<K,V> {
private int hash;
private K key; // 键
private V value; // 值
private Node<K,V> next;
private int idxOfNext; // 数组索引
public Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public Node(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return 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;
}
public int getIdxOfNext() {
return idxOfNext;
}
public void setIdxOfNext(int idxOfNext) {
this.idxOfNext = idxOfNext;
}
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;
}
@Override
public String toString() {
return "Node{" +
"hash=" + hash +
", key=" + key +
", value=" + value +
", next=" + next +
", idxOfNext=" + idxOfNext +
'}';
}
}
Map接口
public interface MyMap<K,V> {
/**
* 插入数据
* @param key
* @param value
*/
void put(K key, V value);
/**
* 获取数据
* @param key
* @return
*/
V get(K key);
}
public class MyHashMap<K, V> implements MyMap<K, V> {
// 数组用于存储值。初始大小设置为8。
private Object[] objects = new Object[8];
/**
* 将键值对存储到映射中。
* 如果键已存在,其对应的值将被新值替换。
*
* @param key 要存储的键
* @param value 对应的值
*/
@Override
public void put(K key, V value) {
// 计算键的哈希码并使用位运算确定数组的索引。
int idx = key.hashCode() & (objects.length - 1);
// 存储值到计算出的索引位置。
objects[idx] = value;
}
/**
* 根据给定的键检索值。
* 如果键不存在,则返回 null。
*
* @param key 要检索的键
* @return 对应的值,如果键不存在则为 null
*/
@Override
public V get(K key) {
// 计算键的哈希码并使用位运算确定数组的索引,然后返回该索引处的值。
return (V) objects[key.hashCode() & (objects.length - 1)];
}
}
举例
public class MyHashMap02BySeparateChaining<K,V> implements MyMap<K, V> {
// 使用一个数组,每个元素都是一个链表,用于存储具有相同哈希索引的多个节点。
private LinkedList<Node<K,V>>[] objects = new LinkedList[8];
/**
* 将键值对添加到哈希映射中。
* 如果发生哈希冲突(即多个键有相同的哈希索引),则使用链表存储这些键值对。
*
* @param key 要添加的键
* @param value 对应的值
*/
@Override
public void put(K key, V value) {
// 计算键的哈希码并确定在数组中的索引。
int idx = key.hashCode() & (objects.length - 1);
// 如果在该索引位置没有链表,则创建一个新链表并添加节点。
if (objects[idx] == null) {
objects[idx] = new LinkedList<>();
objects[idx].add(new Node<K, V>(key, value));
} else {
// 如果链表已存在,则直接在链表末尾添加节点。
objects[idx].add(new Node<K, V>(key, value));
}
}
/**
* 根据键检索对应的值。
* 如果键在哈希映射中不存在,则返回 null。
*
* @param key 要检索的键
* @return 对应的值,如果键不存在则为 null
*/
@Override
public V get(K key) {
// 计算键的哈希码并确定在数组中的索引。
int idx = key.hashCode() & (objects.length - 1);
// 遍历链表,查找与给定键相匹配的节点。
for (Node<K, V> kvNode : objects[idx]) {
if (key.equals(kvNode.getKey())) {
// 如果找到匹配的键,返回对应的值。
return kvNode.getValue();
}
}
// 如果没有找到匹配的键,返回 null。
return null;
}
}
当插入新的键值对时,如果计算出的索引位置已经被占用,它会顺序检查后续的索引,直到找到一个空位。同样地,在获取值时,它会从计算出的索引位置开始检查,直到找到匹配的键或遍历完整个数组。这种方法简单高效,适用于键值对数量较少、冲突较少的情况。
举例
开放寻址就像你开车去一个停车场,每个停车位都对应一个哈希表中的槽位。
哈希冲突:当你按照停车场的某种规则(类似于哈希函数)到达一个特定的停车位时,如果那个位置已经有车了,这就相当于发生了一个“哈希冲突”。
开放寻址:由于你的首选停车位已被占用,你不会就此放弃,而是开始寻找下一个可用的停车位。这个过程就类似于开放寻址法中的探测序列 —— 你按照一定的顺序检查接下来的停车位。
解决冲突:最终,当你找到一个空的停车位时,你就会把车停在那里。这个空位就像是哈希表中通过开放寻址法找到的空槽位,用于解决哈希冲突。
以下是代码示例
public class MyHashMap03ByOpenAddress<K,V> implements MyMap<K,V> {
// 使用 Node 类型的数组来存储键值对。
private Node<K,V>[] objects = new Node[8];
/**
* 向哈希映射中插入一个键值对。
* 如果出现哈希冲突(即计算出的索引位置已被占用),则使用线性探测法找到空闲位置。
*
* @param key 要插入的键
* @param value 对应的值
*/
@Override
public void put(K key, V value) {
// 计算键的哈希码,并根据哈希表大小确定数组索引。
int idx = key.hashCode() & (objects.length - 1);
// 如果计算出的索引位置为空,则直接插入新节点。
if (objects[idx] == null) {
objects[idx] = new Node<>(key, value);
} else {
// 如果该位置已被占用,则循环查找下一个空闲位置。
while (true) {
if (objects[idx] == null) {
objects[idx] = new Node<>(key, value);
break;
}
// 线性探测:索引递增,如果到达数组末尾则循环回到数组开始位置。
idx = (idx + 1) % objects.length;
}
}
}
/**
* 根据键从哈希映射中获取值。
*
* @param key 要查找的键
* @return 返回对应的值,如果未找到则返回 null
*/
@Override
public V get(K key) {
// 计算键的哈希码,并根据哈希表大小确定数组索引。
int idx = key.hashCode() & (objects.length - 1);
while (true) {
// 检查当前索引位置的节点是否为要查找的键。
if (objects[idx] != null && objects[idx].getKey().equals(key)) {
return objects[idx].getValue();
}
// 线性探测:索引递增,如果到达数组末尾则循环回到数组开始位置。
idx = (idx + 1) % objects.length;
}
}
}
合并散列方法是简单高效结局Hash冲突的方法,它不需要额外的结构如链表或树。它适用于键值对数量较少、冲突较少的情况。在实际应用中,这种方法可以快速定位数据,并在冲突发生时有效地解决问题。然而,在有大量数据和频繁冲突的情况下性能将会下降
举例
想象有一家图书馆的书架管理。假设每个书架是一个哈希表中的槽位,每个书架上有一个特定的区域用于放置特定类别的书籍。这类似于通过哈希函数对书籍进行分类。
发生冲突:当一本新书到来,按照分类应该放到已经满了的书架上时,这相当于发生了哈希冲突。
合并散列:为了解决这个问题,图书馆管理员开始寻找最近的空闲书架(或书架上的空闲区域)。管理员将这本新书放在这个空闲位置,并在原本应该放置书籍的那个满了的书架上留下一张便签,标注新书的实际位置。
查找书籍:当读者查找这本书时,他们会先来到书籍原本应该放置的书架,发现便签后,便会根据便签上的指示去找到实际放置书籍的书架。
以下是代码示例
public class MyHashMap04ByCoalescedHashing<K, V> implements MyMap<K, V> {
// 使用 Node 类型的数组来存储键值对。
private Node<K, V>[] objects = new Node[8];
/**
* 向哈希映射中插入一个键值对。
* 如果计算出的索引位置已有元素(即发生哈希冲突),则使用合并散列法处理冲突。
*
* @param key 要插入的键
* @param value 对应的值
*/
@Override
public void put(K key, V value) {
// 计算键的哈希码,并根据数组大小确定索引。
int idx = key.hashCode() & (objects.length - 1);
// 如果该索引位置为空,直接插入新节点。
if (objects[idx] == null) {
objects[idx] = new Node(key, value);
return;
}
// 如果该索引位置的键与要添加的键相同,则替换节点。
if (objects[idx].getKey().equals(key)) {
objects[idx] = new Node(key, value);
return;
}
// 发生哈希冲突,从数组末尾向前查找空闲位置。
int cursor = objects.length - 1;
while (objects[cursor] != null && !objects[cursor].getKey().equals(key)) {
--cursor;
}
// 在找到的空闲位置创建新节点。
objects[cursor] = new Node<>(key, value);
// 更新原有碰撞位置节点,使其指向新节点。
while (objects[idx].getIdxOfNext() != 0) {
idx = objects[idx].getIdxOfNext();
}
objects[idx].setIdxOfNext(cursor);
}
/**
* 根据键从哈希映射中获取对应的值。
* 如果键不存在,则返回 null。
*
* @param key 要查找的键
* @return 对应的值,如果键不存在则为 null
*/
@Override
public V get(K key) {
// 同样使用键的哈希码计算索引。
int idx = key.hashCode() & (objects.length - 1);
// 遍历链表直到找到匹配的键或链表结束。
while (objects[idx] != null && !objects[idx].getKey().equals(key)) {
idx = objects[idx].getIdxOfNext();
}
// 返回找到的值或 null。
return objects[idx] == null ? null : objects[idx].getValue();
}
}
public class Client {
public static void main(String[] args) {
System.out.println("============= 没有解决碰撞 ===============");
MyMap<String, String> map = new MyHashMap<>();
map.put("1","大黄");
map.put("2","大白");
System.out.println("碰撞前 key:"+"1"+" value:" + map.get("1"));
// 下标碰撞
map.put("9","猪宝");
map.put("12","猪猪");
System.out.println("碰撞后 key:"+"1"+" value:" + map.get("1"));
System.out.println("碰撞后 key:"+"12"+" value:" + map.get("12"));
System.out.println("============= 使用开放寻址解决碰撞 ===============");
MyHashMap03ByOpenAddress<String, String> MyHashMapOpenAddress = new MyHashMap03ByOpenAddress<>();
MyHashMapOpenAddress.put("1","大黄");
MyHashMapOpenAddress.put("2","大白");
System.out.println("碰撞前 key:"+"1"+" value:" + MyHashMapOpenAddress.get("1"));
// 下标碰撞
MyHashMapOpenAddress.put("9","猪宝");
MyHashMapOpenAddress.put("12","猪猪");
System.out.println("碰撞后 key:"+"1"+" value:" + MyHashMapOpenAddress.get("1"));
System.out.println("碰撞后 key:"+"12"+" value:" + MyHashMapOpenAddress.get("12"));
System.out.println("============= 使用拉链寻址解决碰撞 ===============");
MyHashMap02BySeparateChaining<String, String> myHashMapSeparateChaining = new MyHashMap02BySeparateChaining<>();
myHashMapSeparateChaining.put("1","大黄");
myHashMapSeparateChaining.put("2","大白");
System.out.println("碰撞前 key:"+"1"+" value:" + myHashMapSeparateChaining.get("1"));
// 下标碰撞
myHashMapSeparateChaining.put("9","猪宝");
myHashMapSeparateChaining.put("12","猪猪");
System.out.println("碰撞后 key:"+"1"+" value:" + myHashMapSeparateChaining.get("1"));
System.out.println("碰撞后 key:"+"12"+" value:" + myHashMapSeparateChaining.get("12"));
System.out.println("============= 使用合并散列解决碰撞 ===============");
MyHashMap04ByCoalescedHashing<String, String> myHashMap04ByCoalescedHashing = new MyHashMap04ByCoalescedHashing<>();
myHashMap04ByCoalescedHashing.put("1","大黄");
myHashMap04ByCoalescedHashing.put("2","大白");
System.out.println("碰撞前 key:"+"1"+" value:" + myHashMap04ByCoalescedHashing.get("1"));
// 下标碰撞
myHashMap04ByCoalescedHashing.put("9","猪宝");
myHashMap04ByCoalescedHashing.put("12","猪猪");
System.out.println("碰撞后 key:"+"12"+" value:" + myHashMap04ByCoalescedHashing.get("12"));
System.out.println("碰撞后 key:"+"1"+" value:" + myHashMap04ByCoalescedHashing.get("1"));
}
}