主要想和大家分享哈希表的相关内容
哈希表是一种拥有随机访问能力的表.
它是通过一个映射函数将元素的内容映射到对应的地址或者位置,从而减少了遍历过程中的消耗.
常见的哈希关系有取余的方法,这也是最简单的.
通过取余的方式我们可以轻易地得到元素对应的位置,那么问题来了,如果该位置已经有元素占用了怎么办(哈希冲突发生)?
常见的做法有两种:
1. 闭散列
如果发生冲突,就沿着哈希表继续往下找,找到下一个空的位置,按照这个思路,存放元素的时候好说,查找和删除可就麻烦了,而且要是冲突比较多,这些操作就相当于遍历数组,效率会很受影响.所以在实际中我们并不会使用这种方式,
2. 开散列
如果某个位置发生冲突,我们就让这个位置变成一个链表或者二叉树.
Java 标准库中的 HashMap 就是采用了这种方式,当某个位置发生冲突,就将这个位置先变成链表,如果冲突严重再将这个位置变成红黑树.
hash 函数到底怎么设计?
实际上我们不用自己设置 hash 函数,完全可以采用现成的
例如 md5 ,md5 主要用于给字符串计算 hash 值,我们前面说数字类型可以取余来完成,但是字符串就不可以这样了,而 md5 可以将字符串的内容变成一个数据,然后再采用取余的方式生成哈希函数即可.
它有如下的特点:
定长:无论输入的数据多长,得到的 md5 都是定长的.
分散:输入的数据稍有变化,得到的 md5 就会相差很大.
不可逆:根据 md5 的值几乎无法返回成原来的字符串.
正是由于这些特点, md5 也经常用来加密.
Java 标准库中的 HashMap 的底层实现就是 哈希表
下面是我根据哈希的思想实现的一个简单的 HashMap
class HashNode {
public int key;
public int value;
public HashNode next;
public HashNode(int key, int value) {
this.key = key;
this.value = value;
}
}
public class MyHashMap {
// 哈希表
private HashNode[] array = new HashNode[16];
// 表中的元素个数
private int size = 0;
// 哈希函数,简单求余
private int hash(int key) {
// 可以设计的更为复杂,比如根据 key 计算 md5 在求余
return key % array.length;
}
// 根据 key 的值查找 value
public Integer get(int key) {
int index = hash(key);
for (HashNode cur = array[index]; cur != null; cur = cur.next) {
if (cur.key == key) {
// 找到了
return cur.value;
}
}
return null;
}
// 根据 key 的值查找 value
// 如果查找不到,返回默认值
public Integer getOrDefault(int key, int defaultVal) {
Integer ret = get(key);
if (ret == null) {
return defaultVal;
}
return ret;
}
// 插入新的键值对
public void put(int key, int value) {
int index = hash(key);
if (array[index] == null) {
// 如果这个位置还没有元素
array[index] = new HashNode(key, value);
}else {
// 这个位置已经有元素了,即发生哈希冲突,
// 我们先在这个位置的链表查找存不存在该 key 值
for (HashNode cur = array[index]; cur != null; cur = cur.next) {
if (cur.key == key) {
// 这个 key 已经存在了,现在修改这个 value 即可
cur.value = value;
return;
}
}
// 循环结束也没有发现和 key 相等的,新建节点,头插在此
HashNode newNode = new HashNode(key, value);
newNode.next = array[index];
array[index] = newNode;
}
size++;
// 到这里插入已经成功了
// 但是可能疯狂插入导致每个链表都很长,使得查找的效率低下,
// 所以当哈希表拥挤到一定程度的时候我们要进行扩容
// loadFactor() 是负载因子 = 元素个数 / 数组长度
if (loadFactor() > 0.75) {
// 扩容
resize();
}
return;
}
private void resize() {
// 使用更大的数组存放
HashNode[] newArray = new HashNode[2 * array.length];
// 搬运旧表上的元素
for (int i = 0; i < array.length; i++) {
HashNode next = null;
for (HashNode cur = array[i]; cur != null; cur = next) {
next = cur.next;
int index = cur.key % newArray.length;
// 插入新表
cur.next = newArray[index];
newArray[index] = cur;
}
}
array = newArray;
}
private double loadFactor() {
return (double) size / array.length;
}
// 根据 key 的值删除对应的键值对
// 删除成功返回 key 对应的 value ,失败返回 null
public Integer remove(int key) {
int index = hash(key);
Integer ret = null;
if (array[index] == null) {
// 这个链表直接为空,查找失败
return null;
}
if (array[index].key == key) {
// 说明待删除的节点是链表头结点
ret = array[index].value;
array[index] = array[index].next;
size--;
return ret;
}
// 待删除的节点可能在中间某个位置
HashNode pre = null;
for (HashNode cur = array[index]; cur != null; pre = cur, cur = cur.next) {
if (cur.key == key) {
// 找到了
ret = cur.value;
pre.next = cur.next;
size--;
return ret;
}
}
// 循环结束都没找到,说明没有
return null;
}
}
核心的操作是插入操作 put
在 put 方法中有两个需要注意的点:
1. 负载因子,负载因子用来衡量一个哈希表的拥挤程度(发生冲突的概率),其值 = 元素个数 / 数组长度,当这个值过大的时候哈希表会进行扩容操作.Java 标准库中这个临界值是 0.75 ,太低表的利用率很低,太高影响查找效率.
2. 扩容函数,当负载因子超过 0.75 时,哈希表会自动进行扩容,将数组的容量变为原来的2倍( Java 标准库中,是以移位的形式进行的).