一、Hash是什么?
Hash也称散列、哈希。其原理就是把任意长度的输入,通过对应的Hash函数(或者叫散列函数)变成固定的输出,所构造出来的结构就叫哈希表(Hash Table)(或者叫散列表)。Hash表的搜索不必进行多次没有必要的操作,大大提高了搜索效率,但这样存在有一定的问题,那就是当两个不同的关键值通过同一个Hash函数计算后得到了相同的Hash地址,又怎么办呢?
我们将这个情况称为Hash冲突或Hash碰撞。把具有不同关键码而具有相同Hash地址的数据元素称为“同义词”。
由于底层的Hash表的数组容量一般是小于实际要存储的数据容量,所以冲突的发生是必然的,我们只能做到降低冲突率。
那么如何做到降低冲突率呢?
我们需要设计一个合理的哈希函数。
下面是哈希函数设计的原则:
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单
常用的有:
直接定制法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B 优点:简单、均匀 缺点:需要事先知道关键字的分布情况。
除留余数法--(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
但是不管怎么样,哈希冲突是必然会发生的,那么我们该如何解决冲突呢?首先我们来了解一下一个概念,那就是散列表的负载因子,负载因子 = 表中的元素个数/散列表的长度。当负载因子越大,发生冲突的概率就越大,所以我们就需要调整哈希表中数组的大小了。
解决冲突最常用的两种方法就是闭散列和开散列两种方法。
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。而寻找下一个空位置也有两种方法:
- 线性探测法:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
- 二次查找法:从发生冲突位置往后找,下一个位置的求法为:Hash(key) = (Hash(key) + d^2) % 11 其中d是距离冲突位置的长度
需要注意的是:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素
开散列:也叫链地址法 、开链法,首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中,当出现重复冲突时,把冲突元素新建一个链表结点,尾插到哈希表中的对应冲突位置的元素后面。当链表的元素过大时,就需要将链表树化,变为红黑树,加强它的搜索效率。
二、关联
Map和Set是一种专门用来查找的容器或者说是一种数据结构,他们的搜索效率都是非常快的,在我们日常的编写代码中经常会用到。
HashMap和HashSet的底层都是用Hash表来实现的。Set只能够存放Key值,Map可以存放Key-Val的键值对,但是两个的Key值都是只能够存在一个相同的值。
这里我们看一下HashSet的add方法的原码。很显然,它是用HashMap的put方法来实现的。所以HashSet的底层就是一个HashMap。
三、Hash表的实现
要实现Hash表,我们先来
public class HashBuck {
static class Node {//hash表中的存储数据的类
public int key;
public int val;
public Node next;//定义成链表的形式,可以往后添加元素
public Node(int key, int val) {
this.key = key;
this.val = val;
}
}
public Node[] array ;//hash表
public int usedSize;//记录 当前哈希桶当中 有效数据的个数
public HashBuck() {
this.array = new Node[10];
this.usedSize = 0;
}
private final float FACTOR = 0.75f;//定义最大负载因子数
//存储key val
public void put(int key,int val) {
Node node = new Node(key,val);//先创建新结点
int index = key%array.length;//利用hash函数计算key值对应的hash位置
Node cur = array[index];
while (cur != null){//当不为空时跳出
if (cur.key == key){//如果有相同的key,那么更新它的val
cur.val = val;
return;
}
cur = cur.next;
}
node.next = array[index];//采用头插法,
array[index] = node;
usedSize++;
float f = usedSize*1.0f/array.length;//看插入后负载因子是否超过了最大限度
if (f >= FACTOR){//如果超过了,就扩容
grow();
}
}
*//**
* 1. 遍历数组的每个元素的链表
* 2. 每遍历到一个节点,就重新哈希 key % len
* 3. 进行头插法
*//*
private void grow() {
Node[] newArray = new Node[2* array.length];//二倍扩容
//重新的哈希
for (Node cur : array){
while (cur != null){
Node curNext = cur.next;
int key = cur.key;
int index = key%newArray.length;
cur.next = newArray[index];
newArray[index] = cur;
cur = curNext;
}
}
this.array = newArray;
}
*//**
* 通过key值 获取val 值
* @param key
* @return
*//*
public int get(int key) {
for (int i = 0; i < array.length; i++) {
Node cur = array[i];
while (cur != null){
if (cur.key == key){
return cur.val;
}
cur = cur.next;
}
}
return -1;
}
总结
这就是我对HashMap和HashSet的一点讲解和认识,希望能够帮助到你。