1. 什么是哈希表
想要查找一个元素方法有很多,比如说查找数组中的某个元素,直接遍历数组,依次判定比较,时间复杂度为O(N);或者将数组排成有序数组,再采用二分查找,时间复杂度为O(Nlog(N));还可以将数组的内容放到二叉搜索树中,时间复杂度为O(Nlog(N))。
那么能不能通过key,不需要遍历和比较,就能直接得到对应的value呢?为了实现这样的思想,于是就有了哈希表,奥秘就是借助了数组取下标的时间复杂度为 O(1),借助了内存的随机访问能力。即将 key 通过一定的数学变换,映射到数组下标上,如下示例:
比如现在要存储如下的key:
直接将key当做数组下标,映射的数组如下:
这样的话,比如查找key=5是否存在,直接判断 arr[5]是否等于1。
如果数字的差异比较大,比如 key 分别为 9,5,2,7,100001。这样的话就很难直接作为数组的下标,所以就要找到某个映射函数(哈希函数),把当前这些数值,经过一定的数学变换,映射到数组下标上,最简单粗暴的方式就是取余。
存储如下的key:
对key取余之后,发现 10w1,1和101%100,结果都是 1,也就意味着这三个key都要被存到下标为 1 的位置上,但是很明显不能这么做,这种情况就是“哈希冲突”。把10w1,1和101 这三个数字称为“同义词”。哈希冲突是客观存在的,难以避免的,下面就来介绍两种解决哈希冲突的方式:闭散列和开散列。
2. 闭散列
如果产生了冲突,去查找下一个空闲位置即可。
比如存 key为:9,5,2,102,103
此时使用数组不再为int数组了,而是使用对象数组,在对象中保存数字的原始值。
102%100=2,下标为2,下标为2的地方已经存有元素了,那么查找下一个空闲位置,下标为3的位置是空闲的,将102放在下标为3的位置上。103%100=3,同样产生了冲突,寻找下一个空闲位置为下标为4的位置。
闭散列最大的问题在于,一旦数组上比较拥挤,此时性能就会下降的特别严重,要想解决这个问题就需要频繁扩容,保持数组尽量稀疏。
3. 开散列/哈希桶
数组上的元素不再是一个单纯的元素,而是一个链表的头结点,也就是数组中的每个元素都是一个链表。一旦出现哈希冲突,就把这个对象插入到对应位置的链表上就行了。
使用开散列的方式解决冲突,可能还会出现某个下标位置的链表特别长的情况,优化手段为:a)尝试扩容 b)把这个较长的链表转换成红黑树/哈希表。
4. 自己实现[put+get],开散列解决哈希冲突
class HashNode{
int key;
int value;
HashNode next;
public HashNode(int key, int value) {
this.key = key;
this.value = value;
}
}
public class MyHashMap {
private HashNode[] arr=new HashNode[16];
//当前存储的元素的个数
private int size=0;
//插入键值对
public void put(int key,int value){
//按照 key % 数组的长度转换下标
int index=key%arr.length;
//判断当前key是否存在,如果存在则直接修改key的value
for(HashNode cur=arr[index];cur!=null;cur=cur.next){
if(cur.key==key){
cur.value=value;
return;
}
}
//如果不存在,直接将其插入到index位置
HashNode newNode=new HashNode(key,value);
newNode.next=arr[index];
arr[index]=newNode;
size++;
//考虑是否需要扩容,负载因子如果超过 0.75,就需要扩容
if(loadFactor()>0.75){
resize();
}
}
//计算负载因子
private double loadFactor(){
return size/arr.length;
}
//扩容,创建一个新的数组,将原数组的内容拷贝到新的下标处
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=next){
next=cur.next;
int newIndex=cur.key%newArr.length;
cur.next=newArr[newIndex];
newArr[newIndex]=cur;
}
}
arr=newArr;
}
//根据 key 得到 value
public Integer get(int key){
int index=key%arr.length;
for(HashNode cur=arr[index];cur!=null;cur=cur.next){
if(cur.key==key){
return cur.value;
}
}
return null;
}
}
测试:
public class Test {
public static void main(String[] args) {
MyHashMap map=new MyHashMap();
map.put(2,90);
map.put(10001,2);
map.put(3333,245);
map.put(100,10);
System.out.println(map.get(10001));
System.out.println(map.get(8));
}
}
负载因子=存储的元素个数/哈希表的长度,负载因子用来衡量当前这个哈希表的拥挤程度。
5. 补充
字符串计算hash值的方法?
md5,md4,sha1,sha256...
特点:
- ① 定长:无论输入的字符串多长,得到的 hash 值都是固定长度
- ② 分散:只要输入的字符串稍微变动一点点,得到的hash值就会差别很大。如果两个字符串str1,str2的md5值相同,那么就可以认为str1和str2相同。
- ③ 不可逆:给定字符串计算md5,比较容易,但是给定 md5值,找到对应的字符原串,理论上是不可能的。
md5的用途:
- ① 作为字符串的hash算法
- ② 加密领域
- ③ 校验文件传输结果是否正确(传输之前计算文件的md5,传输之后计算文件的md5,如果两个 md5 相同,文件传输正确)
若有写的不对的地方,还请各位大佬指点~~