哈希表

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 相同,文件传输正确)

若有写的不对的地方,还请各位大佬指点~~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值