实现一个哈希表(HashMap),以下是对代码的详细解释:
TNode 类:
TNode
类表示哈希表中的节点,包含键(key)、值(value)、下一个节点的引用(next)以及键的哈希值(hash)。TNode(K key, V value, int hash)
用于初始化节点。class TNode {
K key;
V value;
TNode next;
int hash;// K的hash值
public TNode(K key, V value, int hash) {
this.key = key;
this.value = value;
this.hash = hash;
}
}
MyHashMap 类:
MyHashMap
类表示哈希表,使用泛型来支持键值对的存储。table
(哈希表数组)、modCount
(数组被占用的格子数量)、elmSize
(元素个数)、capacity
(数组的容量)。DEFAULT_CAPACITY
和负载因子 LOAD_FACTOR
。public class MyHashMap {
private TNode[] table;
private int modCount;// 数组被占用的格子数量
private int elmSize;// 元素个数
private int capacity;// 数组的容量
private static final int DEFAULT_CAPACITY = 16;
private static final double LOAD_FACTOR = 0.75;
}
构造方法:
MyHashMap(int initCapacity)
: 构造方法用于初始化哈希表数组,根据传入的初始容量设置数组大小。public MyHashMap(int initCapacity) {
if (initCapacity < DEFAULT_CAPACITY) {
initCapacity = DEFAULT_CAPACITY;
}
table = new TNode[initCapacity];
capacity = initCapacity;
elmSize = 0;
modCount = 0;
}
put 方法:
put(K key, V value)
: 用于向哈希表中存储键值对。计算键的哈希值,创建新节点,并根据哈希值和数组长度确定存储位置。处理冲突,若位置为空则直接存储新节点,否则在链表末尾添加新节点。如果数组被占用格子数量超过负载因子,进行扩容。public void put(K key, V value) {
//System.out.println("Put");
int h = key.hashCode();// 计算Key 的hash
TNode elmNode = new TNode<>(key, value, h);// 新节点
int index = h & (capacity - 1);// 根据hash值 与 数组长度 得到下标
// System.out.println(index);
TNode first = table[index];
if (first == null) {
table[index] = elmNode;
modCount++;
elmSize++;
} else {
TNode oldNode = null;
// 如果first 与 新节点的key一致 更新 first节点的v
if (first.hash == h && first.key == key || first.key.equals(key)) {
first.value = value;
oldNode = new TNode<>(key, first.value, h);
} else {
TNode temp = first;
while (temp.next != null) {
temp = temp.next;
if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
temp.value = value;
oldNode = new TNode<>(key, temp.value, h);
break;
}
}
if (oldNode == null) {// 新增节点
temp.next = elmNode;
elmSize++;
}
}
}
// 扩容: 数组的被占用格子数 比例大于数组容量的百分之75的时候扩容
if (modCount >= capacity * LOAD_FACTOR) {
resize();
}
}
resize 方法:
resize()
: 扩容方法,将数组大小加倍,重新哈希并存储旧数组中的所有元素。// 扩容:
// 扩容 2倍扩容
// 创建一个更大的数组 将原本的所有元素存储进去 (每个元素取出 重新映射位置 )
// 每个元素存储的位置与当前数组的长度相关
private void resize() {
//System.out.println("扩容进入:" + capacity);
modCount = 0;
elmSize = 0;
int oldCapacity = capacity;
int newCapacity = oldCapacity + oldCapacity;
TNode[] oldTable = table;
TNode[] newTable = new TNode[newCapacity];
for (int i = 0; i < oldCapacity; i++) {
//System.out.println("扩容循环");
// 取出旧节点
TNode node = oldTable[i];
while (node != null) {
TNode newNode = new TNode<>(node.key, node.value, node.hash);
//System.out.println("遍历旧数组中的链表");
int h = node.hash;
int index = h & (newCapacity - 1);
TNode first = newTable[index];
if (first == null) {
newTable[index] = newNode;
elmSize++;
modCount++;
} else {
//System.out.println("新数组目标位置是一条链表");
TNode temp = first;
while (temp.next != null) {
// System.out.println("temp.next !=null");
// System.out.println(temp.value);
temp = temp.next;
}
temp.next = newNode;
elmSize++;
}
node = node.next;
}
}
table = newTable;
capacity = newCapacity;
System.out.println("扩容:" + capacity);
}
get 方法:
get(K key)
: 用于从哈希表中获取键对应的值。计算键的哈希值,根据哈希值和数组长度确定位置,然后在链表中查找对应的节点。public V get(K key) {
int h = key.hashCode();
int index = h & (capacity - 1);
TNode first = table[index];
// 判断 目标位置的节点是否为null
if (first != null) {
// 判断 first 是否是咱们需要的节点
// 1: 哈希值一致 才需要比较后面的
// 2: 接着比较key的引用地址 如果地址一致 就不需要比较内容
// 3: 地址不一致的情况会存在内容一致的情况 使用equals比较
if (first.hash == h && first.key == key || first.key.equals(key)) {
return first.value;
}
// 第一个节点比较不成功 从这个节点开始作为头节点遍历链表
TNode temp = first;// 底层: 判断 first 是链表节点还是红黑树节点
while (temp.next != null) {
temp = temp.next;
if (temp.hash == h && temp.key == key || temp.key.equals(key)) {
return (V) temp.value;
}
}
}
return null;
}
main 方法:
public static void main(String[] args) {
MyHashMap map = new MyHashMap<>(16);
for (int i = 0; i < 10000; i++) {
map.put("Hello" + i, i);
}
System.out.println("数组长度:" + map.capacity);
System.out.println("数组被占用格子数量:" + map.modCount);
System.out.println("元素个数:" + map.elmSize);
MyHashMap map2 = new MyHashMap<>(16);
for (int i = 0; i < 100; i++) {
map2.put("Hello" + i, i);
}
System.out.println("数组长度:" + map2.capacity);
System.out.println("数组被占用格子数量:" + map2.modCount);
System.out.println("元素个数:" + map2.elmSize);
}
这段代码实现了基本的哈希表功能,包括键值对的存储、冲突处理、扩容等。通过哈希函数将键映射到数组位置,并在需要时进行扩容,确保了哈希表的高效性。
哈希表是一种常见的数据结构,它通过将关键字映射到表中的位置来实现快速的数据检索
哈希函数:
哈希表的核心是哈希函数,它将关键字映射到表中的位置。好的哈希函数应该具有均匀分布的特性,尽量避免冲突。哈希冲突:
哈希冲突指两个不同的关键字被映射到了相同的位置。解决冲突的方法包括链地址法、开放地址法等。链地址法:
使用数组和链表结合的方式,每个数组位置上存储一个链表。哈希冲突时,新的元素加入到对应位置的链表中。在该方法中,哈希表的每个槽位都维护一个链表。当冲突发生时,新的键值对被添加到相应槽位的链表中。这样,具有相同哈希值的键值对都存储在同一个链表上。
优点:简单易实现,适用于任何类型的对象。
缺点:需要额外的空间来存储链表,可能导致内存浪费。
开放地址法:
当发生冲突时,通过一定规则寻找下一个可用的位置,直到找到为止。线性探测是一种开放地址法的冲突解决方法。当哈希表中的某个槽位已被占用时,线性探测会在哈希表中顺序查找下一个可用的槽位,直到找到一个空槽位为止。
详细步骤:
优点:实现简单,易于理解和实现。
缺点:可能导致聚集,即连续的槽位被占用,影响性能。查找速度可能变慢,因为需要线性搜索。
二次探测是另一种开放地址法的冲突解决方法。与线性探测不同,二次探测使用二次方程来计算下一个探测位置,从而减少线性探测可能导致的聚集问题。
详细步骤:
优点:缓解了线性探测可能导致的聚集问题。
缺点:仍然可能存在聚集,性能可能不如其他更复杂的方法。
双重哈希是一种使用两个不同的哈希函数来解决冲突的方法。当发生冲突时,通过第二个哈希函数计算新的槽位。
详细步骤:
优点:减少了聚集的可能性,提高了性能。
缺点:实现相对复杂,需要设计两个哈希函数。需要谨慎选择两个哈希函数,以避免产生相同的探测序列。
装载因子:
装载因子是哈希表中已存储元素个数与数组长度的比值。当装载因子超过一定阈值,通常为0.75,就需要进行扩容操作,以维持较低的冲突率。扩容:
当哈希表需要扩容时,通常会创建一个新的更大的数组,然后重新映射原有的元素到新数组中。这涉及到重新计算哈希函数和解决新的哈希冲突。hashCode和equals方法:
在Java中,要使用自定义对象作为哈希表的键,需要正确实现hashCode
和equals
方法。hashCode
用于计算哈希值,而equals
用于判断两个对象是否相等。实现哈希表的自定义类:
在实现自定义的哈希表时,需要考虑合适的哈希函数、解决冲突的方法,以及正确处理扩容等操作。