目录
介绍
实现哈希表
大体框架
实现数组扩容
实现查询key
实现新增元素
实现删除元素
哈希算法
String中重写的hashCode()方法
哈希表也叫散列表,哈希表是一种数据结构,它提供了快速的插入操作和查找操作,无论哈希表总中有多少条数据,插入和查找的时间复杂度都是为O(1)。在实现哈希表时,如果只靠数组存储,当需要存储大量元素时,系统很难在内存中找到连续的内存空间。因此需要结合链表来存储大量数据,当链表长度过高时,会转化成红黑树。其次哈希码是可以重复的,当重复时根据数据的值来进行区分。接下来以代码的形式来解释
hash码我们暂时自己设置,hash码与数组索引的关系采用取模运算,为了提高效率,我们采用位运算(不熟悉的可以查看另一篇文章),数组长度需要设置为 ,如果数组长度不满足,那么后续的实现代码会存在错误。
public class HashTable {
//定义一个数组
Entry[] table = new Entry[16];
int size = 0; // 元素个数
float loadFactor = 0.75f;//当(size/数组长度) = 0.75时进行扩容
int threshold = (int) (loadFactor * table.length);// 阈值计算
static class Entry{
int hash; // 哈希码
Object key; // 键
Object value; // 值
Entry next;
public Entry(int hash, Object key, Object value) {
this.hash = hash;
this.key = key;
this.value = value;
}
}
}
首先我们需要了解什么时候需要进行扩容,以及扩容后原来元素应该如何处理。
当表中存储的元素到达阈值后,需要进行扩容,而扩容的元素的存储位置也需要发生改变。首先就是对链表拆分,一个旧链表最多拆分成两条新链表,然后分别存储在新数组中的不同位置。拆分规律为,hash码对原数组长度进行与运算后等于 0 的节点拆分成一条链表,不等于 0 的节点拆分成另一条链表。
比如说,当原数组中存在一条hash码为0->8->16->24->32->40-> null 的链表。该链表中的每一个hash值都需要对原数组的长度进行与运行。运算结果如下
0 & 8 = 0
8 & 8 = 8
16 & 8 = 0
24 & 8 = 8
32 & 8 = 0
40 & 8 = 8
因此接下来需要将该链表拆分为如下两条链表。
0->16->32->null
8->24->40->null
拆分后,需要将两条新的链表分别存储在新数组中的原始索引下标位置与原始索引下标+原始数组长度的位置。
具体实现代码如下
private void resize() {
//创建出新数组,并且扩容一倍
Entry[] newTable = new Entry[table.length << 1];
for (int i = 0; i < table.length; i++) {
//拿到每个位置的头元素
Entry head = table[i];
if (head != null) {
//讲链表进行拆分
//记录a链表的头位置
Entry aHead = null;
//记录b链表的头位置
Entry bHead = null;
Entry a = null;
Entry b = null;
while (head != null) {
if ((head.hash & table.length) == 0) {
if (a != null) {
//如果a!=null,那么a的下一个元素设置为head
a.next = head;
} else {
//如果a为null,说明还没有拆分出a链表
aHead = head;
}
//a指针后移
a = head;
} else {
if (b != null) {
b.next = head;
} else {
bHead = head;
}
b = head;
}
head = head.next;
}
//处理拆分链表后结尾指针指向位置
if (a != null) {
a.next = null;
newTable[i] = aHead;
}
if (b != null) {
b.next = null;
//拆分后,a链表仍插入新数组中的原始位置,另一个链表需要插入原始位置加原数组长度的下标位置
newTable[i + table.length] = bHead;
}
}
}
table = newTable;
//更新新的阈值
threshold = (int) loadFactor*table.length;
}
在JDK中的哈希表中,查询元素的参数中并没有hash,但是我们选择了手动提供,因此需要添加该参数。
具体实现代码如下
public Object get(int hash,Object key){
//根据hash码获取在数组中的下标
int index = hash & (table.length - 1);
//根据索引获取到保存在数组中的链表头元素
Entry entry = table[index];
if (entry ==null){
return null;
}
//遍历链表,获取key的值
while(entry!=null){
if (entry.key.equals(key)){
return entry.value;
}
entry = entry.next;
}
return null;
}
如果指定key在哈希表中不存在则新增,如果存在则更新。
具体实现代码如下
public void put(int hash, Object key, Object value) {
int index = hash & (table.length - 1);
Entry entry = table[index];
//如果该位置还没有元素
if (entry == null) {
table[index] = new Entry(hash, key, value);
return;
}
while (true) {
//如果找到了则更新
if (entry.key.equals(key)) {
entry.value = value;
return;
}
if (entry.next == null) {
//如果没找到,添加在链表尾部
break;
}
entry = entry.next;
}
entry.next = new Entry(hash, key, value);
size++;
if (size > threshold){
resize();
}
}
public Object remove(int hash, Object key) {
int index = hash & (table.length - 1);
Entry entry = table[index];
if (entry == null) {
return null;
}
Entry p = entry;
Entry prev = null;
while (p != null) {
if (p.key.equals(key)) {
if (prev == null) {
table[index] = p.next;
} else {
prev.next = p.next;
}
size--;
return p.value;
}
prev = p;
p = p.next;
}
return null;
}
所谓哈希算法就是将任意对象分配一个编号的过程,其中编号是一个有范围限制的数组。而常见的哈希算法有MD5、SHA1、SHA256等。常用的哈希算法是MD5与SHA系列的算法。
最简单的获取hash码的方法是调用JDK中的hashCode()方法。不同对象的hash值可能会相同,但是同一对象的hash值一定相同
如果是不同对象的hash码不同,那么如下代码中s1与s2的哈希码应该是不同的
public static void main(String[] args) {
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
}
但是运行的输出结果为
false
96354
96354
哈希码是一样的,因此我们可以知道String类中,重写了hashCode()方法。具体重写方法如下
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
String中重写的方法中,首先需要将每个对象中的每个value属性(也就是字符串)转换成char数组,然后对数组中的不同位数的字符转化成ASCII码后,乘以一个权重值31(为了避免hash冲突,因此乘以一个质数)后相加。