在链表和数组中,如果我们想要查找一个特定的数值,时间复杂度都是O(n)。那有什么办法能用最快的速度来找到一个特定的元素呢?答案便是“哈希表”。在哈希表中,查找、删除、新增一个元素,时间复杂度都是O(1)。
哈希表(也叫散列表)是一种以键值对的方式来存储元素的数据结构,提供快速的插入和查找功能。哈希表基于数组存储数据,因此能在O(1)时间内定位数据。关键字值(key)通过哈希函数映射为数组下标。缺点就是数组创建后容量固定,如果数据较多需要不断扩展其长度。
一般情况下,为了防止计算得到的哈希值超出tableSize,通过哈希函数拿到哈希值后,还会对哈希值再做一次取模操作:
index = hashCode(key) % tableSize
哈希冲突
上面已经讲了,哈希表中是通过哈希函数计算数组下标的,不同的key计算后有可能得到相同的结果,也就会落到哈希表的同一个索引下标的位置,这种现象叫做哈希冲突。
一般哈希冲突有两种解决方法, 拉链法和线性探测法。
哈希表的实现方式——拉链法
我们都知道数组的特点是寻址容易,但是插入和删除数据困难。而链表的特点是寻址困难,但是插入和删除数据容易。拉链法实现的哈希表综合了两者的特性,是一种寻址容易,插入和删除也容易的数据结构。拉链法实现的哈希表由数组和一堆链表组成,其结构如下图所示:
从上图可以看到,哈希表的左侧是数组,数组的每个成员包括一个指针,指针指向一个链表的头结点。链表可能为空,也可能有多个节点。当出现哈希冲突时,只需要把新节点添加到对应链表最后即可。
我们使用拉链法实现的哈希表存储键值对时,首先会使用哈希函数计算key的哈希值,通过哈希值找到对应的数组下标,然后将键值对添加到对应的链表中。寻找元素时,也是根据键的哈希值,找到特定链表中对应的值。
哈希表的操作
- get(K key):通过特定的关键字拿到其所对应的值
- add(Key key, Value value):将一对新的键值对加入哈希表
- remove(Key key):通过关键字,删除哈希表中的键值对
- getSize():获取当前键值对的数量
- isEmpty():查看哈希表是否为空
java实现哈希表
我们以key为int类型,value为String类型实现拉链法的哈希表。
定义哈希节点
public class HashMap {
/**
* 定义哈希节点
*/
static class HashNode {
Integer key;
String value;
HashNode next;
public HashNode(Integer key, String value) {
this.key = key;
this.value = value;
}
}
// 拉链法左边的数组
private List<HashNode> arrays;
// 哈希数组的长度
private int tableSize;
// 当前存储的键值对数量
private int dataSize;
public HashMap() {
arrays = new ArrayList<>();
tableSize = 10;
dataSize = 0;
// 初始化指针指向的链表
for (int i = 0; i < tableSize; i++) {
arrays.add(i, null);
}
}
}
其中HashNode
就是键值对节点的定义,包含了键(key)、值(value)和指向下一个链表节点的next指针。arrays为上图中拉链法左边的数组,数组中存储着指向对应链表的指针。tableSize为哈希数组的长度,初始时初始化为10。dataSize为当前存储的键值对的数量,初始化为0。
获取数组下标值方法
private int getArrayIndex(Integer key) {
int hashCode = key.hashCode();
return hashCode % tableSize;
}
add方法
add方法将一对键值对存储到哈希表中。在存储之前需要先判断是否已经存储过相同key的键值对,如果已经存储过,则只需要更新value即可;如果相同key的键值对不存在,除了需要把键值对存储到哈希表中,为了保证查询效率,还需要判断哈希表是否需要扩容。
/**
* 核心方法 add:新增键值对
*
* @param key
* @param value
*/
public void add(Integer key, String value) {
int arrayIndex = getArrayIndex(key);
// 获取链表头节点
HashNode header = arrays.get(arrayIndex);
HashNode node = new HashNode(key, value);
// 链表不为空的场景下,需要遍历链表查看是否已经存在同样的key
while (header != null) {
// 已经存在相同的key,则更新value
if (header.key.equals(key)) {
header.value = value;
return;
}
header = header.next;
}
// 不存在相同key的场景,将新节点插入到链表的最前面(链表为空的场景同样走这里)
header = arrays.get(arrayIndex);
node.next = header