1. 前言
给每个数据分配一个编号,放入表格(数组)。
建立编号和表格索引的关系,这样就可以通过编号快速查找数据。
- 理想情况编号当唯一时,表格能容纳所有的数据。
- 现实是不能说为了容纳所有的数据造一个超大的表格,编号也可能重复。
解决:
- 有限长度的数组,以拉链方式存储数据。
- 允许编号适当重复,通过数据自身来进行区分。查找时,顺着单向链表查找即可。
2. 哈希表相关代码
哈希表是数组+链表/数组+红黑树的结合。
如果链表过长,会导致查询速率下降。
JDK8开始,HashMap和HashSet内部实现做了改进,引入了树化机制。具体来说,在HashMap种,当一个桶中的链表长度超过阈值(默认是8)且哈希表的容量大于64时,这个链表会被转换成一颗红黑树,这样可以显著减少查询时间,因为红黑树的查找时间复杂度为O(logn)。需要注意的是,当桶的元素数量减少到一定阈值(通常是6或者更小)以下时,红黑树又会退化成链表。
下面的代码包含了Entry内部类,实现了get,put,remove,resize等方法。
public class HashTable {
// 键值指针和哈希值
public class Entry {
Object key;
Object value;
Entry next;
int hash;
public Entry(Object key, Object value) {
this.key = key;
this.value = value;
}
public Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
// 保证数组的长度是2的倍数,比如:2^1, 2^2, 2^3...
// 是为了方便计算哈希值得到数组索引
// 以及方便将原索引位置的一个链表分为两个链表
Entry[] table = new Entry[16];
// 底层装填元素的个数
int size;
// 装填因子
float loadfactor = 0.75f;
// 阈值,超过该值数组将会扩容
int threshold = (int)(loadfactor*table.length);
private Object get(int hash, Object key) {
// 通过哈希值计算得到数组的索引位置
// 等同于hash % (table.length - 1),但下者的性能更好
int index = hash & (table.length - 1);
Entry p = table[index];
if (p == null) {
return null;
}
while (p != null) {
if(p.key == key) {
return p.value;
}
p = p.next;
}
// 此处表明并没有找到
return null;
}
public Object get(Object key) {
int hash = key.hashCode();
hash = hash ^ (hash >> 16);
return get(hash, key);
}
private void put(int hash, Object key, Object value) {
int index = hash & (table.length - 1);
// 先判断有没有找到,如果有,则更新;没有则添加
Entry p = table[index];
Entry q = null;
if (p == null) {
table[index] = new Entry(key, value);
} else {
while (p != null) {
// 进入该if判断说明已经找到,更新该值即可
if (p.key == key) {
p.value = value;
return;
}
q = p;
p = p.next;
}
// 进行到这里说明还没找到,在链表末尾添加节点
q.next = new Entry(key, value);
}
size++;
// 此时可能会超过阈值,判断需不需要扩容
resize(size);
}
public void put(Object key, Object value) {
int hash = key.hashCode();
hash = hash ^ (hash >> 16);
put(hash, key, value);
}
private Object remove(int hash, Object key) {
int index = hash & (table.length - 1);
Entry p = table[index];
Entry q = null;
if (p == null) {
return null;
}
while (p != null) {
// 找到了
if (p.key == key) {
// 如果要删除的是链表的头节点
if (q == null) {
table[index] = p.next;
} else {
q.next = p.next;
}
size--;
return p.value;
}
q = p;
p = p.next;
}
return null;
}
public Object remove(Object key) {
int hash = key.hashCode();
hash = hash ^ (hash >> 16);
return remove(hash, key);
}
public void resize(int size) {
if (size > threshold) {
// 扩容扩大到原来数组大小的一倍
Entry[] newTable = new Entry[table.length << 1];
threshold = newTable.length;
// 此时需要将原来数组的内容搬到另一个数组上
// 可以遵循规律:每条链表最多可以分为两条链表
// 一条是接到原来的索引位置,另一条接到原来索引位置+table.length的索引位置
for (int i = 0; i < table.length; i++) {
Entry a = null;
Entry b = null;
Entry p = table[i];
while (p != null) {
int index = (p.key.hashCode() & (newTable.length - 1));
// 分配给a链表
if (index == i) {
// 第一次分配给a链表
if (a == null) {
a = p;
} else {
a.next = p;
}
}
// 分配给b链表
else {
// 第一次分配给b链表
if (b == null) {
b = p;
} else {
b.next = p;
}
}
p = p.next;
}
// 把a链表接到新数组的原来的索引位置
// 把b链表接到新数组的原来索引+table.length的位置(此时table还没有更新)
newTable[i] = a;
newTable[i + table.length] = b;
}
table = newTable;
}
}
public void traversal() {
for (int i = 0; i < table.length; i++) {
Entry p = table[i];
while (p != null) {
System.out.print(p.key + " ");
p = p.next;
}
}
}
}
3. 单元测试
import org.junit.Test;
/**
* ClassName : HashTabletest
* Package : PACKAGE_NAME
* Description
*
* @Author HeXua
* @Create 2024/8/10 0:19
* Version 1.0
*/
public class HashTabletest {
@Test
public void test() {
HashTable hashtable = new HashTable();
hashtable.put(1, "AA");
hashtable.put(2, "BB");
hashtable.put(3, "CC");
hashtable.put(4, "DD");
// hashtable.traversal();
// 1 2 3 4
// System.out.println(hashtable.get(1));
// AA
hashtable.remove(1);
hashtable.traversal();
// 2 3 4
}
}