哈希表的引入
- 哈希表是基于数组衍生出来的, 它拥有数组的随机访问能力, 所以哈希表非常高效
- 假设我们现在要在下面这样一个数组中查找 5 这个元素, 哈希表就能以O(1)的时间复杂度实现
具体实现方式如下:
-
首先创建一个boolean[]类型的数组, 先遍历原来的数组, 第一个元素为7 我们就把新数组下标为 7 的内容标记为true, 第二个元素是 2 我们就在新数组下标为 2 的内容里填上true 如此一来新数组就变成了这样
-
这时我们再要查找 5 这个元素只需要访问新数组 下标为 5 的元素, 看内容是true还是false
- 这种方法也有一定的弊端, 好比这样的数组
我们没有办法去创建一个 1 ~ 100001 这么大的数组, 这样太浪费空间了, 所以我们想出了一个办法, 就是想办法创建出一种映射关系, 让数字能够对应到一个指定的下标, 我们就引入了 哈希函数 这一概念.
哈希函数
-
哈希函数的目的就在于把一些数值较大的数字, 通过一系列复杂的数学变换, 去映射到一个简单地数组上. 方便我们通过下标寻找该数字.
-
需要注意的是, 哈希函数只是为了降低两个数字对应到一个下标上的概率, 但是还是会存在一定的概率有可能发生冲突, 而我们把两个或多个数字对应到一个下标上发生的这一类冲突, 称为哈希冲突. 而解决这类冲突的方法就是哈希函数.
-
哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是也无法避免哈希冲突 .
哈希冲突的两种解决方法
1. 闭散列(不常用)
- 好比我们现在有下面这样一个数组, 我们要通过 % 的映射关系去创建一个新的boolean类型的数组, 但是出现了 0 和 20 都对应到下标 0 的位置上, 产生了哈希冲突.
- 解决冲突的方案就是 我们在 0 号下标的后面找 找到一个没有映射关系的位置, 把 20 放进去, 就是下标为 2 的位置.
- 但是这种方案有非常大的弊端, 现在冲突的数字只有两个, 但是万一冲突的数字多了, 我们还要继续把冲突的数字放到后面的下标中 这样在后续找该数字的时候, 会非常难找, 并且也会导致其他数字的位置发生变化.
- 所以我们有一种更好的解决方案, 就是下面的开散列
2. 开散列 / 哈希桶 (重点)
- 还是对于上面那种情况, 开散列的解决措施就没有那么简单粗暴了, 而是将新数组的每个元素都变成一个链表.
- 如果有冲突的元素, 我们就把冲突的元素连接到该链表上
- 代码如下:
private HashNode[] arr = new HashNode[20];
private int size = 0;
//通过这个方法把key映射成下标
private int hashCode(int key){
return key % arr.length;
}
public void put(int key, int val){
// 1. 先计算要放到哪个下标上
int index = hashCode(key);
// 2. 再遍历该下标所在的链表看该 key 是否存在
// 如果存在直接修改 val 的值即可
for(HashNode cur = arr[index]; cur != null; cur = cur.next){
if(key == cur.key){
cur.val = val;
return;
}
}
// 3. 如果该 key 不存在就创建一个新节点,头插到链表上
HashNode newNode = new HashNode(key, val);
newNode.next = arr[index];
arr[index] = newNode;
size++;
// 4. 这时还需要判断数组内元素是不是过于多了, 哈希冲突的可能就会越大
// 我们引入一个 负载因子 的概念
// 就是 元素个数 / 数组长度
// 这里设成 0.75 是因为java标准库中的负载因子就是 0.75
if((double)size / arr.length > 0.75){
resize();
}
}
private void resize() {
//扩容的原理就是创建一个更大的数组, 把原来的数组中的元素搬运到新数组中就行了
HashNode[] newarr = new HashNode[arr.length*2];
for(int i = 0; i < arr.length; i++){
HashNode next = null;
for(HashNode cur = arr[i]; cur != null; cur = cur.next){
next = cur.next;
int newIndex = cur.key % newarr.length;
cur.next = newarr[newIndex];
newarr[newIndex] = cur;
}
}
arr = newarr;
}
public Integer get(int key){
int index = hashCode(key);
for(HashNode cur = arr[index]; cur != null; cur = cur.next){
if(key == cur.key){
return cur.val;
}
}
return null;
}
public void remove(int key){
int index = hashCode(key);
for(HashNode cur = arr[index]; cur != null;){
HashNode pre = cur;
if(key == cur.key){
pre.next = cur.next;
}
pre = cur;
cur = cur.next;
}
}