深入了解哈希表及冲突解决:Java模拟实现
哈希表是一种非常高效的数据结构,通过一个称为哈希函数的函数将键映射到存储桶或槽中,从而实现快速的数据存取。然而,由于不同的键可能会哈希到相同的值——这种现象称为“冲突”——因此需要有效的冲突解决策略。以下是关于哈希表的深入分析以及如何在Java中模拟实现,包括常见的冲突解决方法。
哈希表基础
- 哈希函数:用于将输入(键)转换为整数(哈希值),再将其取模数组大小以得到数组索引。
- 负载因子:表示当前哈希表的填充程度,通常用已存元素个数除以数组大小来表示。当负载因子超过某个阈值时,通常需要调整(如扩容)。
冲突解决策略
链地址法(分离链接法):
- 每个桶(数组中的每个位置)维护一个链表,所有哈希到同一个值的元素都存储在该链表中。
开放地址法(开放定址法):
- 所有元素都存储在数组中,通过探测序列来解决冲突,常用的探测方法有线性探测、二次探测和双重哈希。
Java模拟实现
下面是用Java实现上述两种常用冲突解决方法的示例代码:
1. 链地址法
import java.util.LinkedList;
class HashTableChaining<K, V> {
private class Node {
K key;
V value;
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
private LinkedList<Node>[] table;
private int size;
private static final int DEFAULT_CAPACITY = 10;
public HashTableChaining() {
table = new LinkedList[DEFAULT_CAPACITY];
for (int i = 0; i < table.length; i++) {
table[i] = new LinkedList<>();
}
size = 0;
}
private int hash(K key) {
return (key.hashCode() & 0x7fffffff) % table.length;
}
public void put(K key, V value) {
int index = hash(key);
for (Node node : table[index]) {
if (node.key.equals(key)) {
node.value = value;
return;
}
}
table[index].add(new Node(key, value));
size++;
}
public V get(K key) {
int index = hash(key);
for (Node node : table[index]) {
if (node.key.equals(key)) {
return node.value;
}
}
return null;
}
public boolean remove(K key) {
int index = hash(key);
LinkedList<Node> bucket = table[index];
for (Node node : bucket) {
if (node.key.equals(key)) {
bucket.remove(node);
size--;
return true;
}
}
return false;
}
public int size() {
return size;
}
}
2. 开放地址法(线性探测)
class HashTableOpenAddressing<K, V> {
private class Node {
K key;
V value;
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
private Node[] table;
private int size;
private static final int DEFAULT_CAPACITY = 10;
private static final double LOAD_FACTOR = 0.75;
public HashTableOpenAddressing() {
table = new Node[DEFAULT_CAPACITY];
size = 0;
}
private int hash(K key, int probe) {
return (key.hashCode() & 0x7fffffff + probe) % table.length;
}
public void put(K key, V value) {
if (size >= LOAD_FACTOR * table.length) {
resize();
}
int probe = 0;
int index = hash(key, probe);
while (table[index] != null && !table[index].key.equals(key)) {
probe++;
index = hash(key, probe);
}
if (table[index] == null) {
size++;
}
table[index] = new Node(key, value);
}
public V get(K key) {
int probe = 0;
int index = hash(key, probe);
while (table[index] != null && !table[index].key.equals(key)) {
probe++;
index = hash(key, probe);
}
return table[index] != null ? table[index].value : null;
}
public boolean remove(K key) {
int probe = 0;
int index = hash(key, probe);
while (table[index] != null && !table[index].key.equals(key)) {
probe++;
index = hash(key, probe);
}
if (table[index] != null) {
table[index] = null;
size--;
return true;
}
return false;
}
private void resize() {
Node[] oldTable = table;
table = new Node[oldTable.length * 2];
size = 0;
for (Node node : oldTable) {
if (node != null) {
put(node.key, node.value);
}
}
}
public int size() {
return size;
}
}
小结
- 链地址法适合在负载因子较高时使用,因为影响的是链表长度,不是整体结构。
- 开放地址法在负载因子低时性能更优,但是探测可能耗费更多时间。
在实际使用中,选择合适的哈希表实现和参数设置(如哈希函数、负载因子)是关键,以达到优化性能的目的。Java中的HashMap
类本质上也是采用了链地址法的方式实现的。